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).Valueis the full CSP directive string (e.g.default-src 'self'; object-src 'none'; …).Enforcedecides the header name: whentruethe middleware emitsContent-Security-Policy; whenfalseit emitsContent-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 recordfor immutability and structural equality. CarryingEnforceon 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 returnsCspPolicy(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 bySecurityHeadersMiddleware.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 namedExtensionsin this chapter is the framework's Common.AspireExtensions(the canonical service-defaults bootstrap, service-side). (ADC has no app-localServiceDefaultsExtensions; 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 fromMMCA.Common.Aspire(the service-defaults assembly every running service consumes) so that running services do not pull in the heavyAspire.Hostingtooling 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 constraintIResourceWithEnvironment, IResourceWithWaitSupportstatically 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 setsAuthentication__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 gRPCh2c, but the defaultJwtBearerbackchannelHttpClientsends 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 setsDataSources__{logicalName}__SQLServerConnectionString(line 143), which the double-underscore convention flattens toDataSources:{logicalName}:SQLServerConnectionStringfor the multi-source routing layer; the second setsConnectionStrings__SQLServerConnectionString(line 144), the slot the framework's[Required]validation andAddSqlServerhealth 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 ontoDefault, one context, one change tracker, one migration set per deployed service, while each database still carries its owndbo.OutboxMessages. The ADC AppHost calls it once per service (MMCA.ADC.AppHost/Program.cs:93,116,156,181). (This method was namedWithDataSourcebefore ADR-018; it was renamedWithSQLServerDataSourceforWith*DataSourcenaming 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 anAzureCosmosDBDatabaseResource(fromAddAzureCosmosDB(...).AddCosmosDatabase(...)), then.WithReference+.WaitFor+ three env vars:DataSources__{logicalName}__CosmosConnectionString(line 177, the account connection string the resolver hands toCosmosDbContext.UseCosmos(...)),DataSources__{logicalName}__CosmosDatabaseName(line 178, sinceUseCosmostakes the database name separately from the connection string), andConnectionStrings__CosmosConnectionString(line 179, the[Required]/Defaultfallback). 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 toSqliteDbContext.UseSqlite(...)) andConnectionStrings__SqliteConnectionString(line 210, theDefaultfallback). Note the different signature, afilePathstring 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 optionalgateway?lets monolith deployments still use JWKS by pointing straight at Identity.WithSQLServerDataSourcelives in this shared framework assembly (not an AppHost-local helper) so ADC and Store share one implementation; makinglogicalNamean explicit parameter keeps each service's database identity discoverable by reading the AppHost. The threeWith*DataSourcehelpers share one naming shape so an engine move (ADR-018) is a one-line AppHost change. - Where it's used: AppHost
Program.csinMMCA.ADC.AppHost(andMMCA.Store.AppHost) when wiring the broker, cross-service auth, and per-service databases. ADC wires fourWithSQLServerDataSourcecalls today (MMCA.ADC.AppHost/Program.cs:93,116,156,181). TheWithCosmosDataSource/WithSqliteDataSourcehelpers 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*DataSourcehelpers, thelogicalNamemust match the key the owning entity's config derives (module namespace /[UseDatabase]); a mismatch silently routes that entity to theDefaultsource 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
AddCommonGatewayCorsextension method. - Depends on:
Microsoft.ExtensionsCORS / 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, unlikeMMCA.Common.API.AddCommonCors's allow-listed headers/methods, the production gateway policy allows any header and method while restricting origins toCors:AllowedOrigins. That origin restriction is load-bearing: the CORS spec forbids combiningAllowAnyOrigin()withAllowCredentials(), so to let cookies /Authorizationheaders 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 theCors:AllowedOriginsstring array (defaulting to[], lines 42-44) and buildsWithOrigins(origins).AllowAnyHeader().AllowAnyMethod().AllowCredentials()(lines 45-49). Registered as the default policy, so pair it with a bareapp.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.ADCandMMCA.Store. - Caveats / not-in-source: Outside Development, an unset/empty
Cors:AllowedOriginsyields 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
staticTimeSpan/intproperties. Read by the Common.AspireExtensions(ConfigureHttpClientDefaults) and by the typed gRPC client setup inMMCA.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 inMMCA.Common.Sharedbecause bothMMCA.Common.AspireandMMCA.Common.Grpcmay depend only onSharedunder the layer rules, soSharedis the one place both can read from. - Walkthrough: the resilience window,
AttemptTimeout30 s (line 13),CircuitBreakerSamplingDuration60 s (line 16),TotalRequestTimeout90 s including retries (line 19), andMaxRetryAttemptsdeliberately 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,PooledConnectionLifetime10 min so DNS changes on ACA replica rollover are picked up without a restart (line 34),PooledConnectionIdleTimeout5 min so low-traffic inter-service calls skip the TCP+TLS handshake (line 37), andKeepAlivePingDelay60 s /KeepAlivePingTimeout30 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
staticproperties 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
ExtensionsAddStandardResilienceHandlerandSocketsHttpHandlerconfiguration, and the gRPC typed-client pipeline inMMCA.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
OpenIdConnectMetadataWarmupTaskregistered byAddWarmupReadiness, plus any custom tasks added viaAddWarmupTask<T>()); executed byWarmupHostedService.
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 theMMCA.Common.Outboxsource. - 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.OutboxProcessorpolls 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 anOutboxPollspan plus, under the Azure Monitor distro, a childSqlClientdependency 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"andPollActivityName = "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 chainfor (var current = data; current is not null; current = current.Parent)(line 34); if any ancestor matches bothOperationName == "OutboxPoll"andSource.Name == "MMCA.Common.Outbox"(checking both avoids suppressing an unrelated consumer span that happens to be named "OutboxPoll"), it clearsdata.ActivityTraceFlags &= ~ActivityTraceFlags.Recorded(line 42). ClearingRecordedmakes the batch exporters skip the activity; this processor is registered before the exporters so itsOnEndruns first.- Why real work survives: per-message
OutboxProcessspans 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.
- Two literal constants (lines 20-21):
- Why it's built this way: Sealed. The parent-chain walk catches deeply nested children (the
SqlClientspan 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.AspireExtensionsConfigureOpenTelemetry(line 167), beforeAddOpenTelemetryExporters.
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 setX-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 aconfiguredelegate. - 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);PermissionsPolicydenies geolocation/microphone/camera/payment (line 29);EnableHsts = true(line 32) withHstsValue= 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 omitsscript-src/style-srcso it does not break an HTML/Blazor host that forgot to register a provider (Blazor needsscript-src 'wasm-unsafe-eval', MudBlazor needsstyle-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;, notinit-only) so theconfiguredelegate orIOptionsbinding 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 theContentSecurityPolicyproperty itself. - Where it's used: Bound and registered by SecurityHeadersExtensions
AddCommonSecurityHeaders; read byStaticCspPolicyProviderandSecurityHeadersMiddleware.
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 fullerdefault-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
trueexactly once when all IWarmupTask instances finish, gating the/health/readyendpoint. - 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 untilIsReadyistrue, 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 anintwithVolatile.Read/Interlocked.Exchangerather than abool+lockgives lock-free, correctly-published reads/writes.IsReady(line 15):Volatile.Read(ref _isReady) == 1prevents 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, onlyWarmupHostedService(same assembly) may. - Where it's used:
WarmupReadinessHealthCheck.CheckHealthAsyncreturnsHealthywhenIsReady; registered as the"warmup"check tagged"ready"and surfaced at/health/readyby 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 dynamicconnect-src(e.g. pinning to its API origin at runtime) registers a customICspPolicyProviderbefore callingAddCommonSecurityHeaders, because that method registers the default only viaTryAddSingleton(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, ornullto 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-pinnedconnect-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
BlazorCspPolicyProviderthat pinsconnect-srcto 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-configurationlazily 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 theHttpClientpool. Honest caveat from the remarks: the middleware's ownConfigurationManagercaches 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): readsAuthentication: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 namedHttpClientandGETs 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'sAddWarmupReadiness, partial for the[LoggerMessage]source generator. ReusingIHttpClientFactorymeans the warm-up exercises exactly the connection pool the real auth path uses. - Where it's used: Registered as
IWarmupTaskbyAddWarmupReadinessin the Common.AspireExtensions(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
BackgroundServicethat 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 overallStopwatch, runsTask.WhenAll(tasks.Select(RunOneAsync))(line 25), and, crucially, opens the gate in afinallyblock (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; rethrowsOperationCanceledExceptionwhen the host is stopping (lines 43-45) but otherwise catches every exception and logs at Warning (lines 47-52), with#pragma warning disable CA1031documenting 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; thefinally-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
AddWarmupReadinessin the Common.AspireExtensions(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
IHealthCheckthat reports unhealthy until the WarmupReadinessGate opens, tagged"ready"so it appears at the/health/readyendpoint 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 viaTask.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"byAddWarmupReadiness(lines 106-107); surfaced at/health/readybyMapDefaultEndpoints(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-localServiceDefaultsproject). The other class namedExtensionsin this chapter is the Common.Aspire.HostingExtensions, 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-tunedSocketsHttpHandlerfor all outboundHttpClients. - 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; registersMMCA.Common.Outbox/MMCA.Common.Cqrsmeters and theMMCA.Common.Outboxtrace source, and installsOutboxPollFilterProcessorto 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](theSocketsHttpHandlertuning at lines 73-81, with its rationale comment at lines 62-72, is the codebase's clearest FinOps decision:PooledConnectionLifetime10 min picks up ACA replica DNS rollover;PooledConnectionIdleTimeout5 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):ConfigureOpenTelemetry→AddDefaultHealthChecks→AddWarmupReadiness→AddServiceDiscovery, thenConfigureHttpClientDefaultsadds the standard resilience handler (30 s / 60 s / 90 s, lines 55-57), service discovery, and the tunedSocketsHttpHandler(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 withIncludeFormattedMessage/IncludeScopes; metrics add the two literal MMCA meters (MMCA.Common.Outbox/MMCA.Common.Cqrs, lines 153-154); tracing adds theMMCA.Common.Outboxsource and.AddProcessor(new Telemetry.OutboxPollFilterProcessor())(line 167), placed before exporters so itsOnEndclearsRecordedfirst, and optionally installs aParentBased(TraceIdRatio)sampler when a valid ratio is configured (lines 174-175).TryGetTraceSampleRatio(line 189, internal): reads the optionalTelemetry:TracesSampleRatioknob ([Rubric §31, Cost Efficiency/FinOps]) and returnstrueonly for a value in the open interval (0,1); absent, unparseable, or out-of-range input returnsfalse(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 whenOTEL_EXPORTER_OTLP_ENDPOINTis set and Azure Monitor whenAPPLICATIONINSIGHTS_CONNECTION_STRINGis 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.ServiceDefaultsproject 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'sProgram.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), theICspPolicyProvider, and computes_enableHsts = options.Value.EnableHsts && !environment.IsDevelopment()(line 112) so HSTS is never emitted in dev (where it would pinlocalhostto HTTPS).InvokeAsync(line 116): setsXContentTypeOptions = "nosniff",XFrameOptions,Referrer-Policy,Permissions-Policy; conditionallyStrictTransportSecuritywhen_enableHsts; then asks the provider for a CspPolicy and, if non-null, writesContentSecurityPolicywhenEnforceelseContentSecurityPolicyReportOnly(lines 131-142); finallyawait _next(context). - Why it's built this way: Sealed conventional middleware (constructor +
InvokeAsync). Resolving CSP through the injectedICspPolicyProviderrather than reading settings directly is what makes per-host dynamic policies possible while every other header stays uniform. Computing_enableHstsonce 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 inMMCA.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
nullwhen 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 andEnforceContentSecurityPolicyflag (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 viaTryAddSingletonso a custom provider registered first by a Blazor host wins. Caching the policy in the constructor makesGetPolicyallocation-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
AddCommonSecurityHeadersviaTryAddSingleton; 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) andUseCommonSecurityHeaders(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 (AddCommonSecurityHeadersin service registration,UseCommonSecurityHeadersin the pipeline) is the idiomatic ASP.NET Core split. - Walkthrough,
AddCommonSecurityHeaders(services, configuration?, configure?)(line 157): builds anAddOptions<SecurityHeadersSettings>(); binds the"SecurityHeaders"config section whenconfigurationis supplied (lines 165-168); applies the optionalconfiguredelegate (lines 170-173); thenTryAddSingleton<ICspPolicyProvider, StaticCspPolicyProvider>()(line 175),TryAddis the key: a host that registered a customBlazorCspPolicyProviderbefore 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 onextension(T)members). TheTryAddSingletonordering contract is what makes the per-consumer CSP override work without configuration flags. ADR-023 documents the matching foot-gun: because the default provider isTryAdd-registered, a host must register its customICspPolicyProviderbefore callingAddCommonSecurityHeaders, 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) • Index • ADC Conference - Domain Model & Module Contracts ➡