Skip to content

feat: WithE2eGatewayRateLimitLift AppHost extension and ServiceBusEmulatorFixtureBase (E1 + E2) - #336

Merged
ivanball merged 3 commits into
mainfrom
feat/e1-e2-move-to-common
Sep 1, 2026
Merged

ivanball merged 3 commits into
mainfrom
feat/e1-e2-move-to-common

Conversation

@ivanball

@ivanball ivanball commented Sep 1, 2026 •

Copy link
Copy Markdown
Owner

Implements the two move-to-Common extraction items from the 2026-08-31 drift run, closing both E-items (Docs/Planning/Drift/DriftAnalysis-plan.md: E1 the identical-lift row, E2 the generalization row). It also folds in dependabot PR #335 and carries the v1.178.0 CHANGELOG entry, so this is the single PR for the repo this cycle. No FACTS edit (the drift gate reports FACTS up to date), no consumer changes.

Three commits: the E1/E2 feature, the folded dependency bumps, the changelog.

E1 (P3, identical-lift): WithE2eGatewayRateLimitLift

New method on the existing extension(IResourceBuilder<ProjectResource> service) surface of MMCA.Common.Aspire.Hosting/Extensions.cs, mirroring the shipped sibling WithE2eRegistrationThrottleLift one for one:

IResourceBuilder<ProjectResource> WithE2eGatewayRateLimitLift(bool alsoLiftWhen = false)
  • Reads E2E_LIFT_REGISTRATION_THROTTLE itself, OR-ed with alsoLiftWhen (which absorbs MMCA.ADC's forceWasm disjunct, the only delta between the two inline copies). Untriggered it returns the builder unchanged, so it is inert locally and in production.
  • When triggered it sets the three keys from named internal constants placed next to the existing E2eRegistrationsPerIpPerHour: GatewayRateLimiting__PermitLimit=100000, GatewayRateLimiting__GlobalConcurrencyLimit=10000, MmcaGateway__RateLimiterPolicies__auth-tight__PermitLimit=100000.
  • The four-line justification comment (identical prose in both consumers, citing ADC run 32185349945) now lives in the method's XML docs, so the rationale survives deleting the inline blocks.
  • The trigger variable name is now one shared constant used by both lifts, so they cannot drift onto different switches.

Key-name derivation. The plan asked for the GatewayRateLimiting__ prefix to come from GatewayRateLimitingSettings.SectionName rather than a re-hardcoded literal. Referencing MMCA.Common.Aspire from MMCA.Common.Aspire.Hosting was rejected: the AppHost-tier package would then pull the whole service-defaults graph (Azure Monitor, OpenTelemetry, Key Vault, Serilog, health checks) into every consumer's AppHost, which is exactly the independence Extensions.cs documents at the top of the file and MMCA.Common.Gateway.csproj records for itself. So the section names are mirrored as internal constants, and the test project references all three packages and cross-asserts the emitted keys against GatewayRateLimitingSettings.SectionName plus nameof(PermitLimit) / nameof(GlobalConcurrencyLimit), and GatewaySettings.SectionName plus nameof(RateLimiterPolicies) / nameof(GatewayRoutePolicySettings.PermitLimit). A section or property rename now fails a test instead of silently orphaning the lift.

New tests (Tests/Hosting/MMCA.Common.Aspire.Hosting.Tests/E2eLiftTests.cs, 7): set-when-triggered, no-op when neither trigger fires, no-op on an explicit false, alsoLiftWhen-only OR semantics, the derived-key cross-assert, chaining identity, plus one pinning the sibling lift onto the same trigger variable.

E2 (P3, generalization): ServiceBusEmulatorFixtureBase

New public abstract class ServiceBusEmulatorFixtureBase : IAsyncLifetime in MMCA.Common.Testing, beside the shipped CrossServiceFixtureBase. It owns everything both consumers hand-copied:

  • the pinned emulator container (DefaultEmulatorImage = mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.1, overridable through a virtual EmulatorImage), built inside InitializeAsync rather than in a field initializer so a subclass can be constructed (and its non-container logic unit-tested) with no Docker daemon;
  • the static MassTransit AzureServiceBusTransport.Defaults 1h TTL / auto-delete override the emulator's quota forces;
  • Client / AdminClient / HostAddress, with the admin-plane (5300) connection-string construction factored into a pure static ComposeAdminConnectionString(hostname, mappedAdminPort) carrying its explanatory comment;
  • bounded-phase start and stop: virtual ContainerStartTimeout (4 min) / BusStartTimeout (3 min) / BusStopTimeout (1 min) with the named PHASE 1 / PHASE 2 TimeoutException text, so a hang names its phase instead of being killed at the job timeout with its log discarded;
  • optional bus hosting: virtual ReceiveQueueName (null = clients only, no bus) plus ConfigureReceiveEndpoint, with the Consumed bag on the base, so MMCA.ADC keeps its two-contract endpoint and MMCA.Store gets one.

The sealed subclass, the [CollectionDefinition] class, the integration-event contracts and the assertions stay app-side.

One deliberate deviation from the plan's signature sketch: the hook takes the receive-endpoint configurator (ConfigureReceiveEndpoint(IServiceBusReceiveEndpointConfigurator)) rather than the bus-factory configurator. The base then owns the one-queue decision, which is the load-bearing part of the hardening (one queue provisioned once per run, each extra contract costing only a topic plus a subscription against an admin plane throttled at roughly one operation per second); a subclass cannot accidentally provision a second queue and walk back into the hang.

Dependencies added to MMCA.Common.Testing (the same accepted test-tier weight as its JwtBearer / Testcontainers / Yarp references): Testcontainers.ServiceBus 4.14.0 and Azure.Messaging.ServiceBus 7.20.2 (new central pins, matching both consumers), plus MassTransit.Azure.ServiceBus.Core on the repo's existing v8 8.5.10 pin. MassTransit stays on v8 by policy. Lock files regenerated with --force-evaluate, including the out-of-slnx MMCA.Common.Infrastructure.Redis.Tests; several unrelated locks show only the bookkeeping move of Azure.Messaging.ServiceBus from Transitive to CentralTransitive at the same resolved 7.20.1 (transitive pinning is deliberately off in this workspace, so no version actually changes).

New tests (Tests/Hosting/MMCA.Common.Testing.Tests/ServiceBusEmulatorFixtureBaseTests.cs, 9): the 2.x image pin, the admin connection-string form, the bounded default budgets, the no-bus default, override honouring, the pre-start throw instead of a null client, the entity-default lowering, and a no-op dispose on a fixture that never started. Common's unit tier deliberately needs no Docker (its one container-backed job is the separate redis-integration), so nothing Docker-requiring was added to a job without a daemon; behavioural proof lands in the consumers' broker-parity tiers during adoption. A live round trip was verified locally against a throwaway subclass anyway: both containers up plus topology provisioned in 20.8s, publish and consume through the emulator, admin client confirming the queue, clean teardown in 0.4s.

Folded in: dependabot #335 (minor-and-patch group)

Applied by hand rather than merged or cherry-picked: this branch had already regenerated every lock file for E2's new pins, so the two histories would only have conflicted there. System.Linq.Dynamic.Core 1.7.3 -> 1.7.4 and Meziantou.Analyzer 3.0.190 -> 3.0.200, then every lock file regenerated in package mode with --force-evaluate (the slnx plus the four out-of-slnx projects: UI.Maui, UI.Gallery, UI.E2E.Tests, Infrastructure.Redis.Tests). The lock diff contains those two package names and nothing else: 37 Meziantou entries and 14 System.Linq.Dynamic.Core entries, no new or moved transitives.

Meziantou 3.0.200 survived: the Release build of the solution stays at 0 warnings, so no new rule fires at error severity and the shared .editorconfig baseline needs no delta. #335 stays open and can be closed once this merges.

CHANGELOG

New ## [1.178.0] - 2026-09-01 entry in the existing Keep-a-Changelog shape: an ### Added bullet each for WithE2eGatewayRateLimitLift and ServiceBusEmulatorFixtureBase, and a ### Dependencies section covering the two folded bumps plus the new test-tier pins. FACTS.md deliberately untouched (regenerated post-tag by the release flow).

Verification

  • dotnet build MMCA.Common.slnx -c Release: 0 warnings, 0 errors (re-verified after the analyzer bump).
  • Affected suites run directly (the machine-wide dotnet test zero-discovery bug): Aspire.Hosting.Tests 37/37, Testing.Tests 46/46, Architecture.Tests 175/175, re-run after the bump.
  • FACTS drift gate: up to date, untouched.
  • PublicAPI.Unshipped.txt updated for both packages.

Follow-up (not in this PR)

Adoption is the consumers' sweep after the next release: MMCA.ADC Source/Hosting/MMCA.ADC.AppHost/Program.cs:430-446 and MMCA.Store Source/Hosting/MMCA.Store.AppHost/Program.cs:313-327 replace their inline blocks with one call each (gateway.WithE2eGatewayRateLimitLift(alsoLiftWhen: forceWasm) for ADC, gateway.WithE2eGatewayRateLimitLift() for Store), after which a search for GatewayRateLimiting__ across both AppHosts must return zero hits; both ServiceBusEmulatorFixture classes reduce to a sealed subclass overriding ReceiveQueueName and ConfigureReceiveEndpoint.

🤖 Generated with Claude Code

ivanball and others added 3 commits September 1, 2026 08:16
…latorFixtureBase

Implements the two move-to-Common extraction items (E1, E2) from the
2026-08-31 drift run (Docs/Planning/Drift/DriftAnalysis-plan.md).

E1 (MMCA.Common.Aspire.Hosting): new WithE2eGatewayRateLimitLift on the
ProjectResource extension surface, mirroring the shipped sibling
WithE2eRegistrationThrottleLift. It reads E2E_LIFT_REGISTRATION_THROTTLE
itself, OR-ed with an alsoLiftWhen call-site flag, and when triggered sets
GatewayRateLimiting__PermitLimit, GatewayRateLimiting__GlobalConcurrencyLimit
and MmcaGateway__RateLimiterPolicies__auth-tight__PermitLimit from named
internal constants; otherwise it returns the builder unchanged, so it is
inert locally and in production. The section names are mirrored rather than
referenced (an AppHost-tier package must not take the service-defaults or
YARP graphs to spell a config key, the same posture MMCA.Common.Gateway
records) and a unit test cross-asserts them against
GatewayRateLimitingSettings.SectionName and GatewaySettings.SectionName so a
rename cannot silently orphan the lift. The justification for the lift
(2026-08-18 ADC run 32185349945) moves into the method's XML docs.

E2 (MMCA.Common.Testing): new ServiceBusEmulatorFixtureBase beside
CrossServiceFixtureBase, owning the pinned 2.0.1 emulator container, the
process-global MassTransit v8 entity-default override the emulator's 1h TTL
quota forces, the AMQP and admin-plane (5300) clients, the bounded PHASE 1 /
PHASE 2 start plus bounded stop, and optionally the one MassTransit bus for
the tier (virtual ReceiveQueueName plus ConfigureReceiveEndpoint, with a
Consumed bag on the base). The sealed subclass, its collection definition,
the contracts and the assertions stay app-side. Adds
Testcontainers.ServiceBus 4.14.0 and Azure.Messaging.ServiceBus 7.20.2 pins
(MassTransit.Azure.ServiceBus.Core stays on the repo's v8 8.5.10 pin).

Common-side coverage is unit level (no Docker in the unit tier): 7 tests for
the lift and 9 for the fixture base. Behavioural proof lands in the consumer
adoption sweeps; a live emulator round trip was verified locally against the
base (start 20.8s, publish/consume through the emulator, clean teardown).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…o 3.0.200

Folds dependabot's minor-and-patch group (PR #335) into this branch rather
than merging it separately: this branch already regenerated every lock file
for the E1/E2 work, so the two histories would only conflict there. The pins
are applied by hand and all lock files regenerated in package mode with
--force-evaluate, including the four out-of-slnx projects. The diff carries
nothing but the two package versions.

Meziantou 3.0.200 introduces no new error-severity finding: the Release build
of the solution stays at 0 warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Records the two move-to-Common extractions in this PR (the
WithE2eGatewayRateLimitLift Aspire hosting extension and the
ServiceBusEmulatorFixtureBase in MMCA.Common.Testing) plus the folded
dependency bumps, in the existing Keep-a-Changelog shape. FACTS.md is left
alone: the release flow regenerates it after the tag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ivanball
ivanball merged commit 03f6bb5 into main Sep 1, 2026
13 checks passed
@ivanball
ivanball deleted the feat/e1-e2-move-to-common branch September 1, 2026 13:02
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.

1 participant