Onboarding guide

16. Aspire Orchestration & Service Defaults

This chapter covers the hosting boundary: how the distributed MMCA system is composed and run, locally as a single dotnet run, and in Azure Container Apps (ACA) as a set of independent revisions. Two distinct concerns live side by side here, and it pays to separate them up front. AppHost-side code (MMCA.Common.Aspire.Hosting, MMCA.ADC.AppHost) is the orchestrator: it declares the resource graph, containers, databases, broker, the four services, the gateway, the UI, and wires their dependencies. Service-side code (MMCA.Common.Aspire) is the baseline every running process opts into: OpenTelemetry, health checks, service discovery, HTTP resilience, startup warm-up, and hardened security headers. The two assemblies are deliberately kept apart so a running service never drags in the full Aspire.Hosting tooling graph (MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs:7-16). ADC has no app-local ServiceDefaults project: all four services consume MMCA.Common.Aspire's AddServiceDefaults() directly. Everything in this group is plumbing, but it is the plumbing that makes ADR-006 (database-per-service), ADR-008 (service topology + YARP), and ADR-009 (resilience / RTO-RPO) real at runtime rather than aspirational.

The orchestrator: declaring the resource graph

When you run dotnet run --project Source/Hosting/MMCA.ADC.AppHost, Aspire executes Program.cs top to bottom, building a resource model rather than starting anything immediately. It declares a persistent SQL Server container (MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:14), four databases on it (ADC_Identity / ADC_Conference / ADC_Engagement / ADC_Notification, Program.cs:32-35), Redis (:39), a RabbitMQ broker (:60), and a MailDev SMTP container (:67), then the four service projects, the YARP Gateway pinned to https://localhost:6001 (:248-258), and the Blazor UI pinned to 6002 (:275-287). All of the cross-cutting wiring vocabulary lives in one reusable, cross-app place: the Extensions static class of MMCA.Common.Aspire.Hosting (Level 0, MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs:17). It supplies a handful of fluent helpers on Aspire resource builders: AddMessageBroker, WithBroker, WithJwksDiscovery, and the per-engine data-source helpers WithSQLServerDataSource / WithCosmosDataSource / WithSqliteDataSource (the last two added for polyglot persistence, ADR-018).

.WithSQLServerDataSource(db, "Conference") (MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs:132) is the AppHost manifestation of ADR-006: in one fluent chain it adds .WithReference(database), .WaitFor(database), and injects two connection-string env vars, DataSources__{logicalName}__SQLServerConnectionString for the multi-source routing layer and ConnectionStrings__SQLServerConnectionString for the framework's [Required] validation and AddSqlServer health check (Extensions.cs:143-144). Setting both to the same expression deliberately collapses the logical source onto Default, so each service runs as a clean single-database monolith with one change tracker and one migration set, while still owning its own dbo.OutboxMessages table. The routing types that consume those env vars, DataSourceResolver and EntityDataSourceRegistry, live in the persistence group. The ADC AppHost calls it once per service, .WithSQLServerDataSource(notificationDb, "Notification"), .WithSQLServerDataSource(engagementDb, "Engagement"), and so on (MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:93,116,156,181).

The remaining three helpers complete the orchestration vocabulary. AddMessageBroker() (MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs:33) provisions RabbitMQ with the management plugin (:38, UI at http://localhost:15672); WithBroker(broker) (:51) attaches a service to it with .WithReference(broker), .WaitFor(broker), and MessageBus__Provider=RabbitMq (:59-62), the env var that the consuming service's AddBrokerMessaging reads to select RabbitMQ over InProcessMessageBus (selection driven by MessageBusProvider / MessageBusSettings). WithJwksDiscovery(identity, gateway?) (:82) sets Authentication__JwtBearer__Authority (:110) so the consuming service validates RS256 tokens against Identity's published keys (IJwksProvider / RsaJwksProvider). The non-trivial part, captured in a long inline comment (:96-106), is that it prefers the gateway endpoint over Identity's (:107-109): the three REST services run HTTP/2-only on cleartext (h2c) so gRPC clients can use prior-knowledge negotiation, but the default JwtBearer backchannel HttpClient speaks HTTP/1.1, which Kestrel rejects on an HTTP/2-only endpoint. Routing the JWKS / /.well-known/* fetch through the gateway (which terminates TLS and supports both protocols via ALPN, ADR-008) makes the default backchannel work end-to-end without weakening the services (MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:267-269). This is the runtime embodiment of the cross-service token validation boundary in ADR-004, with the transport caveat spelled out in ADR-012. The Cosmos and SQLite siblings (Extensions.cs:166,198) inject the equivalent per-engine connection-string env vars for the staged polyglot-persistence move (ADR-018) and are layered on top of, not instead of, the SQL Server source.

Startup ordering and the gRPC deadlock-avoidance trick

Aspire's WaitFor builds a startup dependency graph from the resource model: a service does not start until the resources it waits on report healthy (/health/ready for projects, container health for SQL / Redis / RabbitMQ). One subtlety worth internalizing now: Conference and Engagement form a bidirectional gRPC pair (ISessionBookmarkValidationService one way, IBookmarkCountService the other). A reciprocal WaitFor would deadlock, each waiting for the other to be healthy before either can start, so the AppHost deliberately omits the reverse WaitFor on the Conference-to-Engagement edge (MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:210-225) and lets transient "peer not ready" failures self-heal via the Polly resilience pipeline baked into AddTypedGrpcClient. The typed-client and interceptor machinery, JwtForwardingClientInterceptor and GrpcResultExceptionInterceptor, lives in the gRPC group (ADR-007). This is the kind of decision that is invisible until it bites; the AppHost's inline comment is the canonical explanation.

The service baseline: AddServiceDefaults()

Every running host calls one method first in Program.cs: AddServiceDefaults() from the single framework-grade Extensions in MMCA.Common.Aspire (Level 2, MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:23). There is no ADC-local copy; all four ADC services consume this one implementation directly. AddServiceDefaults<TBuilder>() (:33) wires OpenTelemetry, default health checks, warm-up readiness, service discovery (:36-39), and an HTTP-client resilience pipeline (30 s attempt timeout, 60 s circuit-breaker window, 90 s total, :55-58, ADR-009) plus a tuned SocketsHttpHandler. That handler block (Extensions.cs:73-81, with its rationale comment at :62-72) is the clearest FinOps decision in the codebase: PooledConnectionLifetime (10 min) picks up ACA replica DNS rollover, PooledConnectionIdleTimeout (5 min) avoids repeated TLS handshakes, and socket-level keep-alive pings every 60 s keep inter-service TCP connections warm without generating HTTP traffic, so an idle replica stays on idle-vCPU billing (roughly 8x cheaper than active) instead of being woken by health traffic.

MapDefaultEndpoints() (Extensions.cs:268) exposes the probe surface ACA reads: /health (all checks, for humans and dashboards, :270), /alive (only the "live"-tagged "self" check so a downstream SQL outage does not kill the process, :274-277), and /health/ready (:283-286), which reports unhealthy until the warm-up gate opens so ingress holds traffic off a still-warming replica. AddInfrastructureHealthChecks() (:230) conditionally adds Redis (:238) and RabbitMQ (:246) probes only when their connection strings are present, so the same binary runs unchanged in an integration-test environment where those containers are absent. Telemetry-wise, ConfigureOpenTelemetry (:136) adds the MMCA.Common.Outbox activity source (:158) and the MMCA.Common.Outbox / MMCA.Common.Cqrs meters by literal name (:153-154), Aspire has no project reference to the assemblies that define them. It also registers OutboxPollFilterProcessor (Level 0, MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Telemetry/OutboxPollFilterProcessor.cs:15) via .AddProcessor(...) (Extensions.cs:167), a BaseProcessor<Activity> whose OnEnd (OutboxPollFilterProcessor.cs:24) walks each ending span's parent chain (:34) and clears ActivityTraceFlags.Recorded (:42) on anything descended from the recurring OutboxPoll activity, suppressing idle-poll spans (and their auto-instrumented SqlClient children) from export so they do not dominate Application Insights ingestion. It must be registered before the exporters so its OnEnd runs first; real per-message OutboxProcess work uses restored parent contexts and is never a poll descendant, so genuine outbox telemetry survives (the poll machinery, OutboxProcessor, IOutboxSignal, OutboxMessage, lives in the events/outbox group, ADR-003). A second, opt-in cost lever sits alongside it: TryGetTraceSampleRatio (Extensions.cs:189) reads an optional Telemetry:TracesSampleRatio knob and, when a deployed host sets it inside (0,1), installs a ParentBasedSampler(TraceIdRatioBasedSampler(...)) (:175) for head-based trace sampling, cutting the largest observability line item; a typo or an out-of-range value falls back to sampling everything, so a mistake can never silently blackhole all telemetry. Finally, AddOpenTelemetryExporters (:304) enables OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is set (the local Aspire dashboard, :307-312) and Azure Monitor when APPLICATIONINSIGHTS_CONNECTION_STRING is set (:315-320); both can run simultaneously.

Warm-up: defeating ACA cold-start

The warm-up subsystem exists for one concrete failure mode: the "first request fails, second succeeds" pattern on a CPU-throttled idle ACA replica, where lazy initialization (OIDC discovery fetch, connection-pool establishment, JIT) stretches past a client timeout. AddWarmupReadiness() (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:98, folded into AddServiceDefaults) registers four cooperating pieces. IWarmupTask (Level 0, Warmup/IWarmupTask.cs:9) is the unit of startup work, a Name for logs (:12) plus ExecuteAsync (:15). WarmupHostedService (Level 1, Warmup/WarmupHostedService.cs:14) is a BackgroundService that runs all registered tasks in parallel exactly once (:25) and then opens the gate in a finally block (:30), so even if a task throws, the replica is never wedged permanently out of rotation (a transient failure is re-paid lazily by the Polly pipeline on the first real request). WarmupReadinessGate (Level 0, Warmup/WarmupReadinessGate.cs:10) is a thread-safe one-shot flag (Volatile.Read / Interlocked.Exchange over an int, :15,18) consumed by WarmupReadinessHealthCheck (Level 1, Warmup/WarmupReadinessHealthCheck.cs:9, tagged "ready"), which is what makes /health/ready report unhealthy until warm-up finishes. The one built-in task is OpenIdConnectMetadataWarmupTask (Level 1, Warmup/OpenIdConnectMetadataWarmupTask.cs:21): it pre-fetches {authority}/.well-known/openid-configuration (:28-46) to warm DNS/TCP/TLS and the HttpClient pool, so the JwtBearer middleware's own first fetch completes in single-digit milliseconds (the middleware still performs its own fetch, an honest caveat stated in the type's remarks, but over a now-warm connection). Services register additional tasks (output-cache priming, reference data) via AddWarmupTask<T>() (Extensions.cs:120). This whole subsystem is the decision recorded in ADR-025 (startup warm-up + readiness gating): warm-up is wired into AddServiceDefaults so every host gets it, the gate opens even when a task fails (availability over strict warmth), and the missed work is re-paid as a lazy retry on the first real request through the resilience pipeline (ADR-009). Tag-wise this subsystem leans heavily on [Rubric §29, Resilience & Business Continuity] (graceful startup, readiness gating, fail-open warm-up) and [Rubric §17, DevOps & Deployment] (the probe contract the deployment platform consumes).

Security headers and CORS at the host edge

The last boundary in this group hardens the request/response edge. The shared response-header middleware is defined entirely in MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs. SecurityHeadersSettings (Level 0, :17) holds the strongly-typed values, FrameOptions (DENY, :23), ReferrerPolicy (:26), PermissionsPolicy (:29), HSTS (:32,35), and a conservative default CSP baseline (default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none', :46-47) safe for JSON/WebSocket/static API and gateway responses; it deliberately omits script-src/style-src so it does not break an HTML/Blazor host that forgot to register a provider. ICspPolicyProvider (Level 1, :64) is the per-host CSP hook, with StaticCspPolicyProvider (Level 2, :71) as the default that serves the static policy and a Blazor host registering its own dynamic implementation before calling AddCommonSecurityHeaders; CspPolicy (Level 0, :56) is the resolved (Value, Enforce) record that decides between Content-Security-Policy and Content-Security-Policy-Report-Only. SecurityHeadersMiddleware (Level 2, :93) stamps the headers on every response (skipping HSTS in Development, :112), and SecurityHeadersExtensions (Level 3, :149) supplies AddCommonSecurityHeaders (binds the "SecurityHeaders" config section, registers the default provider via TryAddSingleton, :157,175) and UseCommonSecurityHeaders (:180). The whole point, stated in the doc comment, is to centralize what each client-facing host previously hand-rolled, eliminating drift; this is ADR-023. Sitting beside it is GatewayCorsExtensions (Level 0, MMCA.Common/Source/Hosting/MMCA.Common.Aspire/GatewayCorsExtensions.cs:16): AddCommonGatewayCors (:22) registers the reverse-proxy's default CORS policy, allow-any origin in Development (:35-38) but in other environments restricting origins to Cors:AllowedOrigins while allowing any header/method and credentials (:45-49) so cookies and Authorization headers can flow through the gateway safely. It is the gateway counterpart to MMCA.Common.API's stricter per-service AddCommonCors, and pairs with the YARP topology of ADR-008. Relevant tags across this boundary: [Rubric §11, Security] and [Rubric §26, Front-End Security] (defense-in-depth headers and CSP against clickjacking, MIME-sniffing, transport downgrade, and XSS), and [Rubric §10, Cross-Cutting Concerns] (one shared middleware and one shared CORS policy instead of N hand-rolled copies).

How it all fits at runtime

Putting the pieces in sequence: the AppHost declares the graph and injects per-service env vars (WithSQLServerDataSource, WithBroker, WithJwksDiscovery, gRPC WithReferences); each service boots, calls AddServiceDefaults() to opt into telemetry / health / resilience / warm-up, and AddCommonSecurityHeaders + UseCommonSecurityHeaders (plus AddCommonGatewayCors at the gateway) at the edge; Aspire withholds traffic via WaitFor and the /health/ready warm-up gate until each replica is genuinely ready; the warm-up tasks pre-pay the cold paths; and once live, outbound calls ride the resilient, idle-cost-tuned HttpClient, integration events flow through the broker selected by the env vars, and telemetry streams to the OTLP endpoint (local Aspire dashboard) or Azure Monitor (when APPLICATIONINSIGHTS_CONNECTION_STRING is set), minus the suppressed outbox-poll noise and any head-sampled trace fraction. The same env-var-and-abstraction contract is what lets a module run in-process or as an extracted service without code changes (ADR-007 / ADR-008): the AppHost decides topology by what it wires, and the service code stays transport-agnostic. The per-type sections that follow document each of these classes in ascending Level order; the module-registration and message-bus types they reference are in the module system (G14) and events & outbox (G04) chapters.

CspPolicy

MMCA.Common.Aspire · MMCA.Common.Aspire.Security · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:56 · Level 0 · record (sealed)

  • What it is: A resolved Content-Security-Policy: the directive string and whether it is enforced or emitted report-only.
  • Depends on: Nothing first-party. Consumed by ICspPolicyProvider, StaticCspPolicyProvider, and SecurityHeadersMiddleware.
  • Concept introduced: [Rubric §11, Security] assesses HTTP security headers (CSP, HSTS, X-Frame-Options) as defence-in-depth; this record is the unit a policy provider hands the middleware. CSP itself is taught at the middleware section; here the point is the enforce-vs-report split. The whole security-headers pipeline is governed by ADR-023 (centralized security-response-headers middleware with a pluggable CSP).
  • Walkthrough: Positional record CspPolicy(string Value, bool Enforce) (line 56). Value is the full CSP directive string (e.g. default-src 'self'; object-src 'none'; …). Enforce decides the header name: when true the middleware emits Content-Security-Policy; when false it emits Content-Security-Policy-Report-Only (browsers report violations but do not block), the standard way to trial a tightened policy without breaking the page.
  • Why it's built this way: sealed record for immutability and structural equality. Carrying Enforce on the record rather than as a separate provider method keeps the policy and its enforcement mode atomic and inseparable, a provider cannot accidentally return a policy string without saying how to enforce it. ADR-023 makes this two-field shape the contract: the provider returns CspPolicy(Value, Enforce), and report-only is the deliberate degradation path a dynamic provider falls back to when it cannot build a correct policy.
  • Where it's used: Returned by ICspPolicyProvider.GetPolicy; read by SecurityHeadersMiddleware.InvokeAsync.

Extensions

MMCA.Common.Aspire.Hosting · MMCA.Common.Aspire.Hosting · MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs:17 · Level 0 · class (static)

Disambiguation: this is the Common.Aspire.Hosting Extensions (AppHost-side broker, JWKS, and data-source wiring). The only other class named Extensions in this chapter is the framework's Common.Aspire Extensions (the canonical service-defaults bootstrap, service-side). (ADC has no app-local ServiceDefaults Extensions; its services consume the Common.Aspire one directly.)

  • What it is: AppHost-side Aspire extension methods that wire shared cross-cutting infrastructure (RabbitMQ broker, JWKS-based identity discovery, message-bus provider selection, and per-service database routing) onto Aspire project resources for extracted-microservice deployments.
  • Depends on: Aspire.Hosting / Aspire.Hosting.RabbitMQ (IDistributedApplicationBuilder, IResourceBuilder<RabbitMQServerResource>, IResourceBuilder<ProjectResource>, IResourceBuilder<SqlServerDatabaseResource>). Conceptually pairs with MessageBusProvider (the env var it sets), the JWKS auth side (IJwksProvider, RsaJwksProvider), and the multi-source routing in DataSourceResolver / EntityDataSourceRegistry.
  • Concept introduced: [Rubric §7, Microservices Readiness], [Rubric §8, Data Architecture], [Rubric §17, DevOps], [Rubric §11, Security]. This assembly is deliberately separate from MMCA.Common.Aspire (the service-defaults assembly every running service consumes) so that running services do not pull in the heavy Aspire.Hosting tooling package (doc comment lines 6-15). §7 assesses how cleanly a module can be lifted into its own deployable; these helpers are the AppHost edge of that boundary.
  • Walkthrough,
    • const string DefaultBrokerResourceName = "rabbitmq" (line 22).
    • AddMessageBroker(IDistributedApplicationBuilder, name) (line 33): builder.AddRabbitMQ(name).WithManagementPlugin(), one call provisions the broker container with its management UI. Production overrides the connection string via config so the same projects can target Azure Service Bus without an AppHost change.
    • WithBroker<TResource>(service, broker) (line 51): .WithReference(broker) + .WaitFor(broker) + .WithEnvironment("MessageBus__Provider", "RabbitMq"), wires the broker reference, holds the service until the broker is healthy, and selects the RabbitMQ transport in one fluent step. The generic constraint IResourceWithEnvironment, IResourceWithWaitSupport statically restricts the method to capable resources.
    • WithJwksDiscovery<TResource>(service, identity, gateway?) (line 82): .WithReference(identity) + .WaitFor(identity), then a deferred .WithEnvironment(context => …) (line 94) that sets Authentication__JwtBearer__Authority (line 110). It prefers the gateway HTTPS endpoint over Identity's, because (comment lines 96-106) Identity listens HTTP/2-only on cleartext for gRPC h2c, but the default JwtBearer backchannel HttpClient sends HTTP/1.1, which Kestrel rejects on an Http2-only endpoint. The gateway terminates TLS, supports HTTP/1.1 + HTTP/2 via ALPN, and forwards /.well-known/* to Identity, so the metadata fetch works end-to-end. When no gateway is passed it falls back to Identity's HTTPS endpoint (with the HTTP/1.1 caveat), explained in ADR-004 (authentication dual-fetch), ADR-008 (service topology), and ADR-012 (gRPC host transport).
    • WithSQLServerDataSource(service, database, logicalName) (line 132): the AppHost manifestation of ADR-006 (database-per-service). A single fluent chain, .WithReference(database) (Aspire injects the connection string), .WaitFor(database) (service does not start until SQL is healthy), then two .WithEnvironment(...) calls: the first sets DataSources__{logicalName}__SQLServerConnectionString (line 143), which the double-underscore convention flattens to DataSources:{logicalName}:SQLServerConnectionString for the multi-source routing layer; the second sets ConnectionStrings__SQLServerConnectionString (line 144), the slot the framework's [Required] validation and AddSqlServer health checks read. Both are set to the same expression (database.Resource.ConnectionStringExpression) on purpose (per the doc comment, lines 114-131): because the two values are identical, the resolver collapses the logical name onto Default, one context, one change tracker, one migration set per deployed service, while each database still carries its own dbo.OutboxMessages. The ADC AppHost calls it once per service (MMCA.ADC.AppHost/Program.cs:93,116,156,181). (This method was named WithDataSource before ADR-018; it was renamed WithSQLServerDataSource for With*DataSource naming consistency with the two siblings below, a breaking API change swept across consumers in lockstep, ADR-016.)
    • WithCosmosDataSource(service, database, logicalName) (line 166): the Azure Cosmos DB sibling (ADR-018, polyglot persistence). Takes an AzureCosmosDBDatabaseResource (from AddAzureCosmosDB(...).AddCosmosDatabase(...)), then .WithReference + .WaitFor + three env vars: DataSources__{logicalName}__CosmosConnectionString (line 177, the account connection string the resolver hands to CosmosDbContext.UseCosmos(...)), DataSources__{logicalName}__CosmosDatabaseName (line 178, since UseCosmos takes the database name separately from the connection string), and ConnectionStrings__CosmosConnectionString (line 179, the [Required]/Default fallback). Unlike SQL Server, a service typically uses Cosmos for one module alongside its SQL Server source, so this is layered on top of (not instead of) WithSQLServerDataSource.
    • WithSqliteDataSource(service, logicalName, filePath) (line 198): the SQLite sibling (ADR-018). SQLite has no Aspire container resource (it is an in-process file), so this only injects connection-string env vars: DataSources__{logicalName}__SqliteConnectionString (line 209, Data Source=<path> handed to SqliteDbContext.UseSqlite(...)) and ConnectionStrings__SqliteConnectionString (line 210, the Default fallback). Note the different signature, a filePath string instead of a database resource.
  • Why it's built this way: Fluent extensions on IResourceBuilder<T> match the Aspire AppHost idiom; the assembly split keeps service runtimes free of AppHost-only dependencies; the optional gateway? lets monolith deployments still use JWKS by pointing straight at Identity. WithSQLServerDataSource lives in this shared framework assembly (not an AppHost-local helper) so ADC and Store share one implementation; making logicalName an explicit parameter keeps each service's database identity discoverable by reading the AppHost. The three With*DataSource helpers share one naming shape so an engine move (ADR-018) is a one-line AppHost change.
  • Where it's used: AppHost Program.cs in MMCA.ADC.AppHost (and MMCA.Store.AppHost) when wiring the broker, cross-service auth, and per-service databases. ADC wires four WithSQLServerDataSource calls today (MMCA.ADC.AppHost/Program.cs:93,116,156,181). The WithCosmosDataSource/WithSqliteDataSource helpers are available framework plumbing (ADR-018), but no consumer wires them today: the Cosmos/SQLite polyglot trial was reverted to all-SQL, so every ADC and Store service currently runs on SQL Server.
  • Caveats / not-in-source: For all three With*DataSource helpers, the logicalName must match the key the owning entity's config derives (module namespace / [UseDatabase]); a mismatch silently routes that entity to the Default source rather than throwing at startup.

GatewayCorsExtensions

MMCA.Common.Aspire · MMCA.Common.Aspire · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/GatewayCorsExtensions.cs:16 · Level 0 · class (static)

  • What it is: Registers the shared default CORS policy for the reverse-proxy Gateway via a single AddCommonGatewayCors extension method.
  • Depends on: Microsoft.Extensions CORS / Configuration / Hosting (ASP.NET Core, BCL). No first-party types.
  • Concept introduced: [Rubric §11, Security], [Rubric §26, Front-End Security], [Rubric §9, API & Contract Design]. §11/§26 assess a cross-origin policy that stays safe while still allowing credentials. The doc comment (lines 7-15) draws the distinction from a service host's CORS: a reverse-proxy gateway must pass arbitrary client headers through to the services it fronts, so, unlike MMCA.Common.API.AddCommonCors's allow-listed headers/methods, the production gateway policy allows any header and method while restricting origins to Cors:AllowedOrigins. That origin restriction is load-bearing: the CORS spec forbids combining AllowAnyOrigin() with AllowCredentials(), so to let cookies / Authorization headers flow the policy must name explicit origins. This is the browser-facing edge of the Gateway topology (ADR-008).
  • Walkthrough: AddCommonGatewayCors(services, configuration, environment) (line 22) null-guards all three arguments (lines 27-29), then registers a default policy. In Development it allows any origin/header/method (AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(), lines 35-38). Otherwise it reads the Cors:AllowedOrigins string array (defaulting to [], lines 42-44) and builds WithOrigins(origins).AllowAnyHeader().AllowAnyMethod().AllowCredentials() (lines 45-49). Registered as the default policy, so pair it with a bare app.UseCors() (no named policy).
  • Why it's built this way: Credentialed cross-origin traffic requires enumerated origins, so the non-Development branch reads them from config rather than allowing everything; the any-header/any-method latitude reflects the gateway's pass-through role. One shared method keeps the ADC and Store gateways from drifting apart.
  • Where it's used: The YARP Gateway hosts of MMCA.ADC and MMCA.Store.
  • Caveats / not-in-source: Outside Development, an unset/empty Cors:AllowedOrigins yields an empty origin list, which blocks all cross-origin credentialed calls, fail-closed (a misconfiguration denies rather than permits), but the browser client then cannot reach the gateway until origins are configured.

HttpResilienceDefaults

MMCA.Common.Shared · MMCA.Common.Shared.Resilience · MMCA.Common/Source/Core/MMCA.Common.Shared/Resilience/HttpResilienceDefaults.cs:10 · Level 0 · class (static)

  • What it is: The single source of truth for the outbound-HTTP resilience and socket-handler values, shared by the Aspire service defaults and the gRPC typed clients so the two transports cannot drift apart.
  • Depends on: Nothing. Plain static TimeSpan/int properties. Read by the Common.Aspire Extensions (ConfigureHttpClientDefaults) and by the typed gRPC client setup in MMCA.Common.Grpc.
  • Concept introduced: [Rubric §29, Resilience & Business Continuity], [Rubric §12, Performance & Scalability]. §29 assesses graceful degradation under partial failure; these constants are the knobs that shape it (ADR-009, resilience and recovery objectives). This type exists specifically to kill drift: the numbers were previously hand-mirrored in two packages and diverged (the class doc comment at lines 3-9 records the 10 s/30 s library defaults that had crept in on the gRPC side versus the tuned 30 s/90 s on the HTTP side). It lives in MMCA.Common.Shared because both MMCA.Common.Aspire and MMCA.Common.Grpc may depend only on Shared under the layer rules, so Shared is the one place both can read from.
  • Walkthrough: the resilience window, AttemptTimeout 30 s (line 13), CircuitBreakerSamplingDuration 60 s (line 16), TotalRequestTimeout 90 s including retries (line 19), and MaxRetryAttempts deliberately 1 (line 28). The retry count carries the most reasoning (doc comment lines 21-27): the UI service base classes own user-facing retries (their Polly policy makes up to 4 attempts), so if every hop also stacked a full retry budget, a backend brownout would balloon into an up-to-16x request storm (4 outer x 4 inner) at the worst possible moment. One transient-fault retry per hop plus the UI-owned policy bounds the worst case while still absorbing connection blips. The socket-handler values, PooledConnectionLifetime 10 min so DNS changes on ACA replica rollover are picked up without a restart (line 34), PooledConnectionIdleTimeout 5 min so low-traffic inter-service calls skip the TCP+TLS handshake (line 37), and KeepAlivePingDelay 60 s / KeepAlivePingTimeout 30 s for socket-level keep-alives that do not count as ACA user traffic (lines 40, 43).
  • Why it's built this way: Expression-bodied static properties keep the file a flat, greppable ledger. Centralising the numbers means a change to the resilience posture is one edit that both the HTTP and gRPC pipelines observe, which is exactly the drift the doc comment says the class was created to end.
  • Where it's used: The Common.Aspire Extensions AddStandardResilienceHandler and SocketsHttpHandler configuration, and the gRPC typed-client pipeline in MMCA.Common.Grpc.

IWarmupTask

MMCA.Common.Aspire · MMCA.Common.Aspire.Warmup · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Warmup/IWarmupTask.cs:9 · Level 0 · interface

  • What it is: A unit of startup work executed once after the host starts, to eliminate lazy initialisation from the first user request, cache priming, opening connection pools, pre-fetching discovery documents.
  • Depends on: Nothing. Implemented by OpenIdConnectMetadataWarmupTask; run by WarmupHostedService; completion gated by WarmupReadinessGate.
  • Concept introduced: [Rubric §29, Resilience & Business Continuity], [Rubric §12, Performance & Scalability]. §29 assesses proactive elimination of cold-start failure modes. After a deployment (or after an idle Azure Container Apps replica is scaled back up), the first requests hit cold paths: EF model build, connection-pool establishment, JIT, OIDC discovery. Warmup tasks run these eagerly. Per the doc comment (lines 3-8), tasks run in parallel after host start, and failures are logged but do not block the readiness gate, a transient dependency outage must not wedge a replica permanently out of rotation.
  • Walkthrough: string Name (line 12): stable identifier used in structured logs and metrics. Task ExecuteAsync(CancellationToken) (line 15): performs the work.
  • Why it's built this way: Interface (not abstract class) for maximum implementation freedom; the non-fatal-failure contract is enforced by the runner (WarmupHostedService), not the interface, so implementations stay simple.
  • Where it's used: Discovered via DI (built-in OpenIdConnectMetadataWarmupTask registered by AddWarmupReadiness, plus any custom tasks added via AddWarmupTask<T>()); executed by WarmupHostedService.

OutboxPollFilterProcessor

MMCA.Common.Aspire · MMCA.Common.Aspire.Telemetry · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Telemetry/OutboxPollFilterProcessor.cs:15 · Level 0 · class (sealed)

  • What it is: An OpenTelemetry BaseProcessor<Activity> that suppresses outbox poll spans and their descendants from telemetry export, preventing idle polling from dominating Application Insights / Log Analytics ingestion cost.
  • Depends on: OpenTelemetry BaseProcessor<Activity> (NuGet); the Azure Monitor distro's automatic SqlClient instrumentation. Conceptually filters the activities raised by OutboxProcessor on the MMCA.Common.Outbox source.
  • Concept introduced: [Rubric §13, Observability], [Rubric §31, Cost Efficiency/FinOps]. §31 assesses deliberate spend control; this is one of the codebase's clearest FinOps decisions. OutboxProcessor polls every relational outbox table on a recurring cycle (default poll interval; deployed environments push it to 300 s precisely so idle polls cost less). Even an idle poll generates an OutboxPoll span plus, under the Azure Monitor distro, a child SqlClient dependency span; at scale those idle spans would dominate ingestion (and spam the local Aspire dashboard). This processor drops them while leaving real per-message work intact.
  • Walkthrough,
    • Two literal constants (lines 20-21): OutboxActivitySourceName = "MMCA.Common.Outbox" and PollActivityName = "OutboxPoll". Duplicated literals, not references to the Infrastructure constants, because the Aspire package has no project references by design (it must not pull in Infrastructure). The comment flags they must stay in sync.
    • OnEnd(Activity data) (line 24): never throws (returns early on null, a telemetry callback must not crash the host). Walks the in-process parent chain for (var current = data; current is not null; current = current.Parent) (line 34); if any ancestor matches both OperationName == "OutboxPoll" and Source.Name == "MMCA.Common.Outbox" (checking both avoids suppressing an unrelated consumer span that happens to be named "OutboxPoll"), it clears data.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded (line 42). Clearing Recorded makes the batch exporters skip the activity; this processor is registered before the exporters so its OnEnd runs first.
    • Why real work survives: per-message OutboxProcess spans are created with explicit parent contexts restored from stored trace ids, so they are never descendants of the poll span and never matched by the parent-chain walk.
  • Why it's built this way: Sealed. The parent-chain walk catches deeply nested children (the SqlClient span is a grandchild of the poll span). The literal-string duplication is the documented price of keeping the Aspire package reference-free (ADR-003 context: the outbox is the producer of these spans).
  • Where it's used: Registered on the tracing pipeline via .AddProcessor(new Telemetry.OutboxPollFilterProcessor()) in the Common.Aspire Extensions ConfigureOpenTelemetry (line 167), before AddOpenTelemetryExporters.

SecurityHeadersSettings

MMCA.Common.Aspire · MMCA.Common.Aspire.Security · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:17 · Level 0 · class (sealed)

  • What it is: Strongly-typed configuration for SecurityHeadersMiddleware, centralising the security-header values each client-facing host previously hand-rolled.
  • Depends on: Nothing first-party (consumed by StaticCspPolicyProvider and SecurityHeadersMiddleware).
  • Concept introduced: [Rubric §11, Security], [Rubric §34, Architecture Governance]. §11 assesses hardened HTTP response headers. Previously each host set X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS and CSP independently, inviting drift; pulling them into a shared settings class with hardened defaults means a new host inherits the safe values automatically, with per-host overrides via the "SecurityHeaders" config section or a configure delegate.
  • Walkthrough: SectionName = "SecurityHeaders" (line 20). Defaults: FrameOptions = "DENY" (line 23, anti-clickjacking, no framing); ReferrerPolicy = "strict-origin-when-cross-origin" (line 26, leaks no path cross-origin); PermissionsPolicy denies geolocation/microphone/camera/payment (line 29); EnableHsts = true (line 32) with HstsValue = one year incl. subdomains (line 35). ContentSecurityPolicy (lines 46-47) defaults to a conservative hardened baseline, default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none', and deliberately omits script-src/style-src so it does not break an HTML/Blazor host that forgot to register a provider (Blazor needs script-src 'wasm-unsafe-eval', MudBlazor needs style-src 'unsafe-inline'); such hosts register their own ICspPolicyProvider. EnforceContentSecurityPolicy = true (line 50), when false, the middleware emits the policy report-only.
  • Why it's built this way: Sealed and mutable (get; set;, not init-only) so the configure delegate or IOptions binding can mutate defaults before the middleware starts. The conservative CSP-without-script/style default is a careful trade-off, codified in ADR-023 as fail-safe over fail-secure: the shared middleware must never be the thing that blanks out a Blazor app, so the baseline is safe for JSON/WebSocket/static API and Gateway responses and harmless to HTML hosts that supply their own provider (which opt into the strong policy explicitly). The intentionally-incomplete baseline is documented on the ContentSecurityPolicy property itself.
  • Where it's used: Bound and registered by SecurityHeadersExtensions AddCommonSecurityHeaders; read by StaticCspPolicyProvider and SecurityHeadersMiddleware.

Stale-doc note (verified against source): the prior tier guide described the CSP default as just "frame-ancestors 'none'"; the current source default (lines 46-47) is the fuller default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'. Refreshed above.


WarmupReadinessGate

MMCA.Common.Aspire · MMCA.Common.Aspire.Warmup · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Warmup/WarmupReadinessGate.cs:10 · Level 0 · class (sealed)

  • What it is: A thread-safe one-shot flag that WarmupHostedService flips to true exactly once when all IWarmupTask instances finish, gating the /health/ready endpoint.
  • Depends on: Nothing first-party (read by WarmupReadinessHealthCheck, set by WarmupHostedService).
  • Concept introduced: [Rubric §29, Resilience], [Rubric §17, DevOps], [Rubric §13, Observability]. Azure Container Apps (and Kubernetes) readiness probes withhold traffic from a replica until its probe returns healthy. The health check reports unhealthy until IsReady is true, so a fresh replica does not receive production traffic until warm-up completes, avoiding slow first-request spikes.
  • Walkthrough: int _isReady (line 12, 0/1): using an int with Volatile.Read/Interlocked.Exchange rather than a bool + lock gives lock-free, correctly-published reads/writes. IsReady (line 15): Volatile.Read(ref _isReady) == 1 prevents a CPU reading a stale cached value. MarkReady() (line 18): Interlocked.Exchange(ref _isReady, 1), idempotent, safe to call twice.
  • Why it's built this way: Sealed (no subclassing). internal void MarkReady() prevents external callers from prematurely opening the gate, only WarmupHostedService (same assembly) may.
  • Where it's used: WarmupReadinessHealthCheck.CheckHealthAsync returns Healthy when IsReady; registered as the "warmup" check tagged "ready" and surfaced at /health/ready by the Aspire service defaults.

ICspPolicyProvider

MMCA.Common.Aspire · MMCA.Common.Aspire.Security · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:64 · Level 1 · interface

  • What it is: The contract for resolving the Content-Security-Policy for a response. The framework ships a static default; a host that needs a dynamic policy registers its own implementation.
  • Depends on: CspPolicy (return type); HttpContext (BCL).
  • Concept introduced: [Rubric §11, Security] (CSP as defence-in-depth) and [Rubric §26, Front-End Security] (CSP protecting Blazor pages from XSS). The doc comment (lines 58-63) explains the extensibility hook: a Blazor host needing a dynamic connect-src (e.g. pinning to its API origin at runtime) registers a custom ICspPolicyProvider before calling AddCommonSecurityHeaders, because that method registers the default only via TryAddSingleton (so a pre-registered custom provider wins).
  • Walkthrough: Single method CspPolicy? GetPolicy(HttpContext context) (line 67): returns the policy to emit for the current response, or null to emit none (per-request, so a provider can vary policy by path or request properties).
  • Why it's built this way: A one-method interface is the minimal extension point for the per-consumer CSP allow-list; returning the CspPolicy record (not a bare string) carries the enforce/report decision atomically. ADR-023 explains why this indirection exists at all: one static CSP cannot serve both a JSON/Gateway host and a Blazor/MudBlazor host (the latter needs script-src 'wasm-unsafe-eval', style-src 'unsafe-inline', and a runtime-pinned connect-src), and a wrong CSP hard-breaks the app, so the policy must be resolved through a provider rather than baked in as a constant.
  • Where it's used: Implemented by the default StaticCspPolicyProvider; per ADR-023 both apps' Blazor UI hosts register a BlazorCspPolicyProvider that pins connect-src to the configured API/Gateway origin and degrades to Report-Only if that origin cannot be resolved. Resolved per-request inside SecurityHeadersMiddleware.

OpenIdConnectMetadataWarmupTask

MMCA.Common.Aspire · MMCA.Common.Aspire.Warmup · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Warmup/OpenIdConnectMetadataWarmupTask.cs:21 · Level 1 · class (sealed, internal, partial)

  • What it is: The built-in IWarmupTask that pre-fetches the OIDC discovery document on startup, warming the network path so the first authenticated request does not hit a cold connection.
  • Depends on: IWarmupTask; IHttpClientFactory, IConfiguration, ILogger<T> (BCL/DI).
  • Concept introduced: [Rubric §29, Resilience & Business Continuity]. The doc comment (lines 6-20) explains the failure mode: without warm-up, the JWT bearer middleware fetches {authority}/.well-known/openid-configuration lazily on the first authenticated request; on a CPU-throttled idle ACA Consumption replica that fetch can exceed the client timeout, the textbook "first request fails, second succeeds". Pre-fetching warms DNS, TCP, TLS, and the HttpClient pool. Honest caveat from the remarks: the middleware's own ConfigurationManager caches separately, so it still performs its own fetch on the first request, but over the now-warm connection that completes in single-digit milliseconds.
  • Walkthrough: Primary constructor injects IHttpClientFactory, IConfiguration, ILogger (line 21). Name => "OpenIdConnectMetadata" (line 26). ExecuteAsync (line 28): reads Authentication:JwtBearer:Authority (line 30); returns early (no-op) if unset (line 31), so non-authenticating hosts incur nothing; builds the discovery URI, logging a warning and returning if it is not a valid absolute URI (lines 36-43); creates a named HttpClient and GETs the document (lines 45-46), logging the status code. Uses source-generated [LoggerMessage] (lines 51-57) for zero-allocation logging.
  • Why it's built this way: internal sealed partial, internal because it is wired by the framework's AddWarmupReadiness, partial for the [LoggerMessage] source generator. Reusing IHttpClientFactory means the warm-up exercises exactly the connection pool the real auth path uses.
  • Where it's used: Registered as IWarmupTask by AddWarmupReadiness in the Common.Aspire Extensions (line 98); executed in parallel by WarmupHostedService.

WarmupHostedService

MMCA.Common.Aspire · MMCA.Common.Aspire.Warmup · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Warmup/WarmupHostedService.cs:14 · Level 1 · class (sealed, internal, partial)

  • What it is: The BackgroundService that runs every registered IWarmupTask exactly once on startup, in parallel, then opens the WarmupReadinessGate.
  • Depends on: IWarmupTask (the set it runs), WarmupReadinessGate (it opens); BackgroundService, ILogger<T>, Stopwatch (BCL).
  • Concept introduced: [Rubric §29, Resilience & Business Continuity] (startup readiness gate), [Rubric §13, Observability] (timing logs per task and overall). The defining design choice: the gate is opened even if tasks fail.
  • Walkthrough: ExecuteAsync (line 19): starts an overall Stopwatch, runs Task.WhenAll(tasks.Select(RunOneAsync)) (line 25), and, crucially, opens the gate in a finally block (gate.MarkReady(), line 30) so a stuck or failing task can never keep the replica out of rotation permanently. RunOneAsync (line 35): times each task; rethrows OperationCanceledException when the host is stopping (lines 43-45) but otherwise catches every exception and logs at Warning (lines 47-52), with #pragma warning disable CA1031 documenting that swallowing a general exception is intentional, a transient warm-up failure self-heals via the Polly retry pipeline on the first real request. [LoggerMessage] source generation for the three log lines.
  • Why it's built this way: internal sealed; the finally-open-the-gate pattern is the resilience invariant from IWarmupTask. Distinguishing host-cancellation (rethrow) from task failure (log + continue) keeps shutdown clean while making warm-up best-effort.
  • Where it's used: Registered as a hosted service by AddWarmupReadiness in the Common.Aspire Extensions (line 102).

WarmupReadinessHealthCheck

MMCA.Common.Aspire · MMCA.Common.Aspire.Warmup · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Warmup/WarmupReadinessHealthCheck.cs:9 · Level 1 · class (sealed, internal)

  • What it is: An IHealthCheck that reports unhealthy until the WarmupReadinessGate opens, tagged "ready" so it appears at the /health/ready endpoint ACA readiness probes hit.
  • Depends on: WarmupReadinessGate; IHealthCheck (BCL).
  • Concept introduced: [Rubric §13, Observability], [Rubric §29, Resilience]. This is the bridge between the in-process gate and the platform: the readiness probe's HTTP result is driven by the gate's boolean. Cross-reference WarmupReadinessGate for the readiness-probe concept.
  • Walkthrough: Single method CheckHealthAsync (line 11): gate.IsReady ? HealthCheckResult.Healthy("Warm-up complete.") : HealthCheckResult.Unhealthy("Warm-up in progress."), returned via Task.FromResult (synchronous, reading a volatile int has no async work).
  • Why it's built this way: internal sealed; trivially cheap (no I/O) so the readiness probe can poll it frequently without cost. Primary-constructor injection of the gate.
  • Where it's used: Registered as the "warmup" check tagged "ready" by AddWarmupReadiness (lines 106-107); surfaced at /health/ready by MapDefaultEndpoints (line 268).

Extensions

MMCA.Common.Aspire · MMCA.Common.Aspire · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:23 · Level 2 · class (static)

Disambiguation: this is the framework (Common.Aspire) Extensions, the canonical service-defaults bootstrap, consumed by every running service (including all four ADC services, there is no ADC-local ServiceDefaults project). The other class named Extensions in this chapter is the Common.Aspire.Hosting Extensions, the AppHost-side broker / JWKS / data-source wiring.

  • What it is: AddServiceDefaults<TBuilder>() / AddWarmupReadiness() / MapDefaultEndpoints(app): the shared Aspire service-defaults bootstrap applied to every framework-consuming service host. Configures OpenTelemetry (logs, metrics, tracing), health checks (/health, /alive, /health/ready), service discovery, the warm-up readiness pipeline, and a Polly resilience pipeline plus a FinOps-tuned SocketsHttpHandler for all outbound HttpClients.
  • Depends on: IWarmupTask, OpenIdConnectMetadataWarmupTask, WarmupHostedService, WarmupReadinessGate, WarmupReadinessHealthCheck, OutboxPollFilterProcessor; plus Azure Monitor, OpenTelemetry, Polly, Microsoft.Extensions.Http.Resilience (NuGet).
  • Concept introduced, Aspire service defaults as a shared cross-cutting bootstrap. [Rubric §13, Observability] (centralised OpenTelemetry + health endpoints; registers MMCA.Common.Outbox/MMCA.Common.Cqrs meters and the MMCA.Common.Outbox trace source, and installs OutboxPollFilterProcessor to drop noisy idle-poll spans before export). [Rubric §29, Resilience] (Polly on every outbound client, 30 s attempt / 60 s circuit-breaker window / 90 s total, ADR-009). [Rubric §31, Cost Efficiency/FinOps] (the SocketsHttpHandler tuning at lines 73-81, with its rationale comment at lines 62-72, is the codebase's clearest FinOps decision: PooledConnectionLifetime 10 min picks up ACA replica DNS rollover; PooledConnectionIdleTimeout 5 min avoids repeated TLS handshakes; socket keep-alive pings every 60 s keep TCP alive without counting as ACA user traffic, so the replica stays on idle-vCPU billing ≈8× cheaper than active: the inline comment is the rationale).
  • Walkthrough,
    • AddServiceDefaults (line 33): ConfigureOpenTelemetryAddDefaultHealthChecksAddWarmupReadinessAddServiceDiscovery, then ConfigureHttpClientDefaults adds the standard resilience handler (30 s / 60 s / 90 s, lines 55-57), service discovery, and the tuned SocketsHttpHandler (lines 73-81).
    • AddWarmupReadiness (line 98): registers the singleton WarmupReadinessGate, the WarmupHostedService, the built-in OpenIdConnectMetadataWarmupTask, and the WarmupReadinessHealthCheck tagged "ready". AddWarmupTask<TTask> (line 120) lets a consumer add service-specific warm-ups.
    • ConfigureOpenTelemetry (line 136): logs with IncludeFormattedMessage/IncludeScopes; metrics add the two literal MMCA meters (MMCA.Common.Outbox / MMCA.Common.Cqrs, lines 153-154); tracing adds the MMCA.Common.Outbox source and .AddProcessor(new Telemetry.OutboxPollFilterProcessor()) (line 167), placed before exporters so its OnEnd clears Recorded first, and optionally installs a ParentBased(TraceIdRatio) sampler when a valid ratio is configured (lines 174-175).
    • TryGetTraceSampleRatio (line 189, internal): reads the optional Telemetry:TracesSampleRatio knob ([Rubric §31, Cost Efficiency/FinOps]) and returns true only for a value in the open interval (0,1); absent, unparseable, or out-of-range input returns false (sample everything), so a typo can never silently drop all telemetry.
    • AddDefaultHealthChecks (line 212) registers the "self" check tagged "live"; AddInfrastructureHealthChecks (line 230) conditionally adds Redis and RabbitMQ checks only when their connection strings are present, so the same binary runs unchanged where those containers are absent.
    • MapDefaultEndpoints (line 268): /health (all), /alive ("live" only), and /health/ready (everything except "live"-only, so the warm-up gate and untagged dependency checks gate traffic).
    • AddOpenTelemetryExporters (line 304, private): enables OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is set and Azure Monitor when APPLICATIONINSIGHTS_CONNECTION_STRING is set, both can run simultaneously.
  • Why it's built this way: One bootstrap means a baseline change (new meter, tighter timeout, the poll-filter) propagates to every consumer in lockstep. There is a single framework copy (no per-app variants), ADC deleted its template-generated MMCA.ADC.ServiceDefaults project and consumes this one directly, so the warm-up gate, the MMCA meters, the poll filter, and the Azure Monitor exporter branch are uniform across every service.
  • Where it's used: builder.AddServiceDefaults() early in each framework-consuming service host's Program.cs.

SecurityHeadersMiddleware

MMCA.Common.Aspire · MMCA.Common.Aspire.Security · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:93 · Level 2 · class (sealed)

  • What it is: ASP.NET Core middleware that adds hardened security response headers to every response: X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy, HSTS (outside Development), and a CSP resolved from ICspPolicyProvider.
  • Depends on: SecurityHeadersSettings, ICspPolicyProvider, CspPolicy; RequestDelegate, IWebHostEnvironment, IOptions<T> (BCL).
  • Concept introduced, centralized security headers as a shared middleware. [Rubric §11, Security] (X-Frame-Options, CSP, HSTS, Referrer-Policy, Permissions-Policy, all defence-in-depth), [Rubric §10, Cross-Cutting Concerns] (the doc comment line 90 states it "Centralizes what each client-facing host previously hand-rolled"), [Rubric §26, Front-End Security] (CSP restricts content sources, reducing XSS impact on Blazor pages).
  • Walkthrough: Constructor (line 101) captures the next delegate, settings (from IOptions), the ICspPolicyProvider, and computes _enableHsts = options.Value.EnableHsts && !environment.IsDevelopment() (line 112) so HSTS is never emitted in dev (where it would pin localhost to HTTPS). InvokeAsync (line 116): sets XContentTypeOptions = "nosniff", XFrameOptions, Referrer-Policy, Permissions-Policy; conditionally StrictTransportSecurity when _enableHsts; then asks the provider for a CspPolicy and, if non-null, writes ContentSecurityPolicy when Enforce else ContentSecurityPolicyReportOnly (lines 131-142); finally await _next(context).
  • Why it's built this way: Sealed conventional middleware (constructor + InvokeAsync). Resolving CSP through the injected ICspPolicyProvider rather than reading settings directly is what makes per-host dynamic policies possible while every other header stays uniform. Computing _enableHsts once in the constructor avoids re-checking the environment per request. This single middleware is ADR-023's "one hardened default, defined once" decision: centralising the header set removes the per-host drift between Gateway and UI (and between apps) that previously came from each edge host hand-rolling its own headers, and makes a new edge host secure by default.
  • Where it's used: Registered into the pipeline by SecurityHeadersExtensions UseCommonSecurityHeaders; per ADR-023 it is wired at both edges of both apps (the Gateway host and the Blazor UI host of Store and ADC). Covered by SecurityHeadersMiddlewareTests in MMCA.Common.Aspire.Tests.

StaticCspPolicyProvider

MMCA.Common.Aspire · MMCA.Common.Aspire.Security · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:71 · Level 2 · class (sealed, internal)

  • What it is: The default ICspPolicyProvider: returns the static CSP configured in SecurityHeadersSettings, or null when none is configured.
  • Depends on: ICspPolicyProvider (implements), SecurityHeadersSettings (via IOptions), CspPolicy (returns).
  • Concept introduced: Cross-reference ICspPolicyProvider for the provider extension point. [Rubric §11, Security]: this is the safe fall-back so a host that registers nothing still gets the hardened static baseline.
  • Walkthrough: Constructor (line 75): reads options.Value.ContentSecurityPolicy; if null/whitespace, _policy = null (no CSP emitted); otherwise builds a CspPolicy capturing the string and EnforceContentSecurityPolicy flag (lines 79-81), computed once at construction, not per request. GetPolicy(HttpContext) (line 84) simply returns the cached _policy (the static provider ignores the request context).
  • Why it's built this way: internal sealed; the framework's default registered via TryAddSingleton so a custom provider registered first by a Blazor host wins. Caching the policy in the constructor makes GetPolicy allocation-free on the hot path. Per ADR-023 this static baseline is the right complete policy for JSON/WebSocket/static hosts (API, Gateway) and a deliberately safe-but-partial fallback for an HTML host that forgot to register a fuller provider.
  • Where it's used: Registered by SecurityHeadersExtensions AddCommonSecurityHeaders via TryAddSingleton; resolved per-request by SecurityHeadersMiddleware.

SecurityHeadersExtensions

MMCA.Common.Aspire · MMCA.Common.Aspire.Security · MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:149 · Level 3 · class (static)

  • What it is: The registration + pipeline extensions for the common security-headers middleware: AddCommonSecurityHeaders (DI) and UseCommonSecurityHeaders (pipeline).
  • Depends on: SecurityHeadersSettings, ICspPolicyProvider, StaticCspPolicyProvider, SecurityHeadersMiddleware.
  • Concept reinforced, middleware registration via extension methods. [Rubric §11, Security] (assesses HTTP security headers) and [Rubric §26, Front-End Security] (CSP defending the Blazor front-end). The two-call shape (AddCommonSecurityHeaders in service registration, UseCommonSecurityHeaders in the pipeline) is the idiomatic ASP.NET Core split.
  • Walkthrough,
    • AddCommonSecurityHeaders(services, configuration?, configure?) (line 157): builds an AddOptions<SecurityHeadersSettings>(); binds the "SecurityHeaders" config section when configuration is supplied (lines 165-168); applies the optional configure delegate (lines 170-173); then TryAddSingleton<ICspPolicyProvider, StaticCspPolicyProvider>() (line 175), TryAdd is the key: a host that registered a custom BlazorCspPolicyProvider before this call keeps it; otherwise the static default is used.
    • UseCommonSecurityHeaders(app) (line 180): app.UseMiddleware<SecurityHeadersMiddleware>(). Call it early in the pipeline so headers are on every response.
  • Why it's built this way: Static class with extension-style methods is the codebase's standard DI/registration idiom (see the primer on extension(T) members). The TryAddSingleton ordering contract is what makes the per-consumer CSP override work without configuration flags. ADR-023 documents the matching foot-gun: because the default provider is TryAdd-registered, a host must register its custom ICspPolicyProvider before calling AddCommonSecurityHeaders, or the static default silently wins.
  • Where it's used: Called by each client-facing host (and the framework service-defaults flow) to register and mount the middleware; pairs the StaticCspPolicyProvider default (or a host's own provider) with SecurityHeadersMiddleware.

⬅ Common UI Framework (MudBlazor components, theme, base pages)IndexADC Conference - Domain Model & Module Contracts ➡