feat: WithE2eGatewayRateLimitLift AppHost extension and ServiceBusEmulatorFixtureBase (E1 + E2) - #336
Merged
Merged
Conversation
…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>
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.
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):
WithE2eGatewayRateLimitLiftNew method on the existing
extension(IResourceBuilder<ProjectResource> service)surface ofMMCA.Common.Aspire.Hosting/Extensions.cs, mirroring the shipped siblingWithE2eRegistrationThrottleLiftone for one:E2E_LIFT_REGISTRATION_THROTTLEitself, OR-ed withalsoLiftWhen(which absorbs MMCA.ADC'sforceWasmdisjunct, the only delta between the two inline copies). Untriggered it returns the builder unchanged, so it is inert locally and in production.E2eRegistrationsPerIpPerHour:GatewayRateLimiting__PermitLimit=100000,GatewayRateLimiting__GlobalConcurrencyLimit=10000,MmcaGateway__RateLimiterPolicies__auth-tight__PermitLimit=100000.Key-name derivation. The plan asked for the
GatewayRateLimiting__prefix to come fromGatewayRateLimitingSettings.SectionNamerather than a re-hardcoded literal. ReferencingMMCA.Common.AspirefromMMCA.Common.Aspire.Hostingwas 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 independenceExtensions.csdocuments at the top of the file andMMCA.Common.Gateway.csprojrecords 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 againstGatewayRateLimitingSettings.SectionNameplusnameof(PermitLimit)/nameof(GlobalConcurrencyLimit), andGatewaySettings.SectionNameplusnameof(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 explicitfalse,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):
ServiceBusEmulatorFixtureBaseNew
public abstract class ServiceBusEmulatorFixtureBase : IAsyncLifetimeinMMCA.Common.Testing, beside the shippedCrossServiceFixtureBase. It owns everything both consumers hand-copied:DefaultEmulatorImage=mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.1, overridable through a virtualEmulatorImage), built insideInitializeAsyncrather than in a field initializer so a subclass can be constructed (and its non-container logic unit-tested) with no Docker daemon;AzureServiceBusTransport.Defaults1h TTL / auto-delete override the emulator's quota forces;Client/AdminClient/HostAddress, with the admin-plane (5300) connection-string construction factored into a purestatic ComposeAdminConnectionString(hostname, mappedAdminPort)carrying its explanatory comment;ContainerStartTimeout(4 min) /BusStartTimeout(3 min) /BusStopTimeout(1 min) with the named PHASE 1 / PHASE 2TimeoutExceptiontext, so a hang names its phase instead of being killed at the job timeout with its log discarded;ReceiveQueueName(null = clients only, no bus) plusConfigureReceiveEndpoint, with theConsumedbag 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.ServiceBus4.14.0 andAzure.Messaging.ServiceBus7.20.2 (new central pins, matching both consumers), plusMassTransit.Azure.ServiceBus.Coreon the repo's existing v88.5.10pin. MassTransit stays on v8 by policy. Lock files regenerated with--force-evaluate, including the out-of-slnxMMCA.Common.Infrastructure.Redis.Tests; several unrelated locks show only the bookkeeping move ofAzure.Messaging.ServiceBusfromTransitivetoCentralTransitiveat 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 separateredis-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.Core1.7.3 -> 1.7.4 andMeziantou.Analyzer3.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
.editorconfigbaseline needs no delta. #335 stays open and can be closed once this merges.CHANGELOG
New
## [1.178.0] - 2026-09-01entry in the existing Keep-a-Changelog shape: an### Addedbullet each forWithE2eGatewayRateLimitLiftandServiceBusEmulatorFixtureBase, and a### Dependenciessection 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).dotnet testzero-discovery bug): Aspire.Hosting.Tests 37/37, Testing.Tests 46/46, Architecture.Tests 175/175, re-run after the bump.PublicAPI.Unshipped.txtupdated 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-446and MMCA.StoreSource/Hosting/MMCA.Store.AppHost/Program.cs:313-327replace their inline blocks with one call each (gateway.WithE2eGatewayRateLimitLift(alsoLiftWhen: forceWasm)for ADC,gateway.WithE2eGatewayRateLimitLift()for Store), after which a search forGatewayRateLimiting__across both AppHosts must return zero hits; bothServiceBusEmulatorFixtureclasses reduce to a sealed subclass overridingReceiveQueueNameandConfigureReceiveEndpoint.🤖 Generated with Claude Code