diff --git a/assets/data/search-index.json b/assets/data/search-index.json index 7adae49..a877ad7 100644 --- a/assets/data/search-index.json +++ b/assets/data/search-index.json @@ -1 +1 @@ -{"v":1,"n":1296,"r":[{"u":"/docs/adr/index.html","d":"Architecture Decision Records","k":"Architecture Decision Records","x":"Accepted ADRs explaining why the core cross-cutting patterns exist. Read these before changing a pattern they describe: they capture context and trade-offs that aren't obvious…","i":"ApplicationLayer_DoesNotUseRawQueryableSurfaces AsyncMethodsDeclareTrailingCancellationToken MessageBusSettings.EnableDelayedRedelivery PushNotificationSettings.ChannelKeyPattern Microsoft.CodeAnalysis.PublicApiAnalyzers RegisterUpcastedIntegrationEventConsumer ApiParameterDescriptorBackfillProvider IWriteRepository.SetOriginalRowVersion ServiceInfoVersioningContractTestsBase ApplicationDbContext.OnModelCreating MMCA.Common.LayerEnforcement.targets CurrentUserTargetingContextAccessor"},{"u":"/docs/adr/index.html#writing-a-new-adr","d":"Architecture Decision Records","k":"Architecture Decision Records","t":"Writing a new ADR","x":"Copy the structure of an existing record: Status (Proposed / Accepted / Superseded, date and link when superseding), Context (the forces and the problem), Decision (what we…"},{"u":"/docs/adr/001-manual-dto-mapping.html","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records"},{"u":"/docs/adr/001-manual-dto-mapping.html#status","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Status","x":"Accepted. Mechanism clarified 2026-06-26: the per-entity mappers are Riok.Mapperly source-generated (compile-time), not hand-written line by line. The decision to avoid runtime…"},{"u":"/docs/adr/001-manual-dto-mapping.html#context","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Context","x":"Domain entities must be mapped to DTOs for API responses. The two common approaches are: 1. Manual mapping classes (IEntityDTOMapper ) 2. Convention-based reflection mapping…","i":"IEntityDTOMapper TEntity TDTO TId"},{"u":"/docs/adr/001-manual-dto-mapping.html#decision","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Decision","x":"Use explicit, per-entity DTO mappers (each a Riok.Mapperly [Mapper] partial class whose MapToDTO body is source-generated at compile time) registered via Scrutor assembly…","i":"IEntityRequestMapper IEntityDTOMapper SpeakerDTOMapper TIdentifierType TCreateRequest UserMapping TEntityDTO MapToDTOs UseMapper MapToDTO partial TEntity"},{"u":"/docs/adr/001-manual-dto-mapping.html#rationale","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Rationale","x":"- Compile-time safety: Mapping errors surface at build time, not runtime. Property renames break the build rather than silently mapping null. - Testability: Each mapper is a…","i":"SpeakerDTOMapper MapToDTO null"},{"u":"/docs/adr/001-manual-dto-mapping.html#trade-offs","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Trade-offs","x":"- More files (30 DTO mappers across Store + ADC: 19 in ADC, 11 in Store, plus the parallel IEntityRequestMapper classes). The interface's default MapToDTOs implementation is…","i":"IEntityRequestMapper MapToDTOs"},{"u":"/docs/adr/002-navigation-populators.html","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records"},{"u":"/docs/adr/002-navigation-populators.html#status","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Status","x":"Accepted"},{"u":"/docs/adr/002-navigation-populators.html#context","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Context","x":"The application supports multiple database backends (SQL Server, Cosmos DB, SQLite). EF Core's .Include() works for SQL Server but fails for Cosmos DB cross-container…","i":"IDataSourceService.HaveIncludeSupport NavigationMetadataProvider declaringType IsCollection Navigation targetType Include"},{"u":"/docs/adr/002-navigation-populators.html#decision","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Decision","x":"Each entity that has unsupported navigations gets a INavigationPopulator implementation. A DeclarativeNavigationPopulator base class (added in MMCA.Common) allows populators to…","i":"DeclarativeNavigationPopulator ChildNavigationDescriptor FKNavigationDescriptor INavigationDescriptor INavigationPopulator NavigationLoader Product.Category Event.Rooms TEntity WHERE"},{"u":"/docs/adr/002-navigation-populators.html#rationale","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Rationale","x":"- Multi-DB support: The query pipeline automatically falls back from Include to NavigationPopulator when the data source reports navigations as unsupported. - Batch efficiency:…","i":"DeclarativeNavigationPopulator"},{"u":"/docs/adr/002-navigation-populators.html#trade-offs","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Trade-offs","x":"- Extra abstraction layer for SQL Server (where Include works fine). Mitigated: the populator is only called when the query pipeline's metadata says navigations are unsupported.…","i":"NullNavigationPopulator"},{"u":"/docs/adr/003-outbox-dual-dispatch.html","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#status","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-07-19 (integration-event routing via IMessageBus, lease-based claims for safe scale-out, dead-letter visibility, post-commit dispatch; see Revision below).…","i":"IMessageBus"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#context","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Context","x":"Domain events must be reliably published after aggregate changes are persisted. Two failure modes exist: 1. In-process dispatch fails (e.g., handler throws): the event is lost if…"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#decision","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Decision","x":"Use a dual-dispatch strategy: 1. Outbox persistence: Domain events are serialized into OutboxMessage rows within the same database transaction as the aggregate changes. This…","i":"DomainEventDispatcher BackgroundService SaveChangesAsync OutboxProcessor OutboxMessage"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#rationale","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Rationale","x":"- Guaranteed delivery: The outbox table is written atomically with the aggregate changes. Even if the process crashes after persistence, the background processor catches up. -…","i":"OutboxPollFilterProcessor ProcessingDelaySeconds BrokerMessageBus OutboxProcessor BrokerEventBus IMessageBus OutboxPoll"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#trade-offs","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Trade-offs","x":"- Domain event handlers must be idempotent (this is a good practice regardless). - The outbox table grows until processed entries are cleaned up: OutboxCleanupService purges rows…","i":"OutboxCleanupService HasMoreEligibleWork ProcessedOn MaxRetries RetryCount"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-07-19","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-07-19)","x":"Four changes from the 2026-07-19 full review: 1. Integration events route through the outbox to IMessageBus, never local dispatch. An IIntegrationEvent raised via AddDomainEvent…","i":"DomainEventSaveChangesInterceptor outbox.dead_letter.count OutboxCleanupService ExecuteUpdateAsync IIntegrationEvent type_unresolvable integrationEvent OutboxProcessor AddDomainEvent OutboxMessage IMessageBus LockedUntil"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-07-24","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Three capture-side corrections found in a code review. None change the dual-dispatch decision; they close gaps between what it promised and what the interceptor did. 1. Capture…","i":"ExecuteInTransactionAsync RemoveDomainEvents IAggregateRoot SavingChanges RetryCount DbContext LastError catch"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-08-01","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"One retry-pacing correction. The dual-dispatch decision is unchanged; the Trade-offs above described a cadence the processor no longer has. 1. Retry backoff is explicit, and it…","i":"RetryBackoffBaseSeconds"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-08-07","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"One retry-pacing refinement. The decision and the curve are unchanged; the waits are no longer identical across a batch. 1. The retry backoff carries random jitter. The…"},{"u":"/docs/adr/004-authentication-dual-fetch.html","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records"},{"u":"/docs/adr/004-authentication-dual-fetch.html#status","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Status","x":"Accepted."},{"u":"/docs/adr/004-authentication-dual-fetch.html#context","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Context","x":"When the modular monolith is extracted into per-module service hosts behind a gateway (ADR-008), every service must authenticate the same end-user JWT, but only one service…"},{"u":"/docs/adr/004-authentication-dual-fetch.html#decision","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Decision","x":"Validate cross-service tokens with asymmetric (RS256) signatures plus JWKS / OIDC discovery, keeping the symmetric (HS256) shared-secret path as the in-process monolith default.…","i":"TokenValidationParameters.ValidAlgorithms id_token_signing_alg_values_supported OpenIdConnectMetadataWarmupTask JwtSettings.SigningAlgorithm BuildValidationParameters MapOidcDiscoveryEndpoint response_types_supported AddCommonAuthentication subject_types_supported AddForwardedJwtBearer WithJwksDiscovery RsaPublicKeyPath"},{"u":"/docs/adr/004-authentication-dual-fetch.html#rationale","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Rationale","x":"- No shared signing key. Only Identity can mint tokens; every other service holds only the public key it fetched, so a compromised non-Identity service cannot forge tokens, and…"},{"u":"/docs/adr/004-authentication-dual-fetch.html#trade-offs","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Trade-offs","x":"- More moving parts than a shared secret. RS256 needs key generation, distribution of the public half, a JWKS endpoint, and discovery wiring, versus one symmetric string. -…"},{"u":"/docs/adr/004-authentication-dual-fetch.html#related","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Related","x":"ADR-007 (gRPC calls forward the validated JWT downstream via JwtForwardingClientInterceptor), ADR-008 (the extraction that split issuer and validator into separate processes),…","i":"JwtForwardingClientInterceptor"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#status","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Status","x":"Accepted. Updated 2026-06-27 (documented the [Pii] → IAnonymizable build-time guard and PiiRedactor).","i":"IAnonymizable PiiRedactor Pii"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#context","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Context","x":"The framework's default deletion model is soft-delete: AuditableBaseEntity.Delete() sets IsDeleted = true and EF Core global query filters exclude the row from normal queries.…","i":"AuditableBaseEntity.Delete OutboxMessage IsDeleted true"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#decision","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Decision","x":"Separate the two concerns and provide an extension point for each, rather than overloading soft-delete: 1. Soft-delete stays the default for lifecycle/state management (hide +…","i":"MMCA.Common.Domain.Attributes.PiiAttribute MMCA.Common.Domain.Interfaces MMCA.Common.Domain.Privacy EncryptedStringConverter PiiConventionTestsBase OutboxCleanupService IAnonymizable PiiRedactor Anonymize Result User Pii"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#rationale","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Rationale","x":"- Right tool per concern: soft-delete answers \"is this record active?\"; erasure answers \"has this person's data been removed?\". Conflating them (e.g. hard-deleting inside…","i":"Delete"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#trade-offs","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Trade-offs","x":"- Erasure is opt-in per entity, but the opt-in is guarded: an aggregate exposing a [Pii]-marked property that does not implement IAnonymizable fails the architecture fitness…","i":"IAnonymizable Pii"},{"u":"/docs/adr/006-database-per-service.html","d":"ADR-006: Database per Service","k":"Architecture Decision Records"},{"u":"/docs/adr/006-database-per-service.html#status","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-07). Supersedes the earlier \"deliberately one shared database\" stance. Clarified 2026-06-27: the single context class became one sealed context class per engine…","i":"Name"},{"u":"/docs/adr/006-database-per-service.html#context","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Context","x":"When the modules were first extracted into independently-deployable services, all services in an app still pointed at a single shared SQL database with a single OutboxMessages…","i":"CrossDataSourceDegradeConvention EntityDataSourceRegistry DataSourceResolver DbContextFactory OutboxProcessor OutboxMessages"},{"u":"/docs/adr/006-database-per-service.html#decision","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Decision","x":"Adopt database-per-service: each service owns its own physical database with its own OutboxMessages table. - One sealed concrete context class per engine, one instance per…","i":"CrossDataSourceDegradeConvention PhysicalDbContextFactory ApplicationDbContext INavigationPopulator DataSourceResolver SQLServerDbContext ADC_Notification CosmosDbContext OutboxProcessor SqliteDbContext ADC_Conference ADC_Engagement"},{"u":"/docs/adr/006-database-per-service.html#rationale","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Rationale","x":"- Removes the shared-outbox race (the sharpest cost of the shared DB) without an OriginService filter: physical isolation is simpler and stronger than a logical filter. - Real…","i":"OriginService"},{"u":"/docs/adr/006-database-per-service.html#trade-offs","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Trade-offs","x":"- No cross-database FKs or transactions. Relationships that span services degrade to scalar IDs; consistency across services is eventual (outbox + broker), not transactional. -…"},{"u":"/docs/adr/007-grpc-extraction.html","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records"},{"u":"/docs/adr/007-grpc-extraction.html#status","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Status","x":"Accepted."},{"u":"/docs/adr/007-grpc-extraction.html#context","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Context","x":"Once modules became separate service processes, the in-process interface calls between them (e.g. Conference → Engagement's IBookmarkCountService, Engagement → Conference's…","i":"ISessionBookmarkValidationService IBookmarkCountService Result"},{"u":"/docs/adr/007-grpc-extraction.html#decision","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Decision","x":"Use gRPC, exposed through MMCA.Common.Grpc, with a contract-package convention: - .Contracts projects hold the .proto definitions plus a gRPC adapter that implements the same…","i":"SessionBookmarkValidationServiceGrpcAdapter GrpcResultExceptionInterceptor JwtForwardingClientInterceptor Directory.Build.props AddTypedGrpcClient SocketsHttpHandler MMCA.Common.Grpc HandleFailure IReadOnlyList RpcException serviceName Contracts"},{"u":"/docs/adr/007-grpc-extraction.html#rationale","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Rationale","x":"- No business-logic rewrite: the gRPC adapter implements the interface modules already depend on; swapping in-process for cross-process is a registration change. - Transport…","i":"MicroserviceExtractionTests ServiceContract MassTransit version proto"},{"u":"/docs/adr/007-grpc-extraction.html#trade-offs","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Trade-offs","x":"- Bidirectional pairs need care. Conference ↔ Engagement is a mutual gRPC pair; the AppHost deliberately omits a reciprocal WaitFor to avoid a startup deadlock: transient \"peer…","i":"Http1AndHttp2 WaitFor Http2"},{"u":"/docs/adr/008-service-extraction-topology.html","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records"},{"u":"/docs/adr/008-service-extraction-topology.html#status","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Status","x":"Accepted. Amended by ADR-089 (2026-08-18): the Gateway keeps the route-to-service map this record gave it, but stops expressing it as MapForwarder calls in code. YARP…","i":"MapForwarder ReverseProxy"},{"u":"/docs/adr/008-service-extraction-topology.html#context","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Context","x":"ADC began as a modular monolith: one MMCA.ADC.WebAPI host loaded every module (Identity, Conference, Engagement, Notification) in-process via the ModuleLoader, sharing one…","i":"MMCA.ADC.WebAPI ModuleLoader"},{"u":"/docs/adr/008-service-extraction-topology.html#decision","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Decision","x":"Extract one service host per module: MMCA.ADC.{Identity,Conference,Engagement,Notification}.Service and front them with a single YARP reverse-proxy Gateway (MMCA.ADC.Gateway,…","i":"MicroserviceExtractionTests MMCA.ADC.Gateway MMCA.ADC.WebAPI ModuleLoader Notification Conference Engagement Identity MMCA.ADC Modules Service Module"},{"u":"/docs/adr/008-service-extraction-topology.html#rationale","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Rationale","x":"- No business-logic rewrite. Because a service is just the monolith with one module enabled, extraction was a hosting/wiring change, not a domain change, and the module-isolation…","i":"ModuleLoader"},{"u":"/docs/adr/008-service-extraction-topology.html#trade-offs","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Trade-offs","x":"- Operational complexity. Four deployables plus a Gateway, service discovery, a broker, and per-service databases, versus one process. Mitigated locally by Aspire orchestration…","i":"MMCA.Common.API ServiceDefaults Http1AndHttp2 Http2"},{"u":"/docs/adr/008-service-extraction-topology.html#applicability","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Applicability","x":"This ADR is framed around ADC (the first repo extracted), but the same topology is now the framework's standard extraction shape, not an ADC-only choice. MMCA.Store followed it:…","i":"MMCA.Store.Gateway MMCA.Store.WebAPI MMCA.Store Identity Catalog Service Sales"},{"u":"/docs/adr/008-service-extraction-topology.html#related","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (outbox dual dispatch), ADR-004 (cross-service token validation via JWKS), ADR-006 (database per service), and ADR-007 (gRPC cross-service calls) are the facet decisions…"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#status","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-14). Amended by ADR-087 (2026-08-18): the resilience objective extends past outbound HTTP and gRPC clients for the first time, to the outbox's broker publish,…"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#context","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Context","x":"The framework already supplies the mechanisms for surviving partial failure: a standard Polly resilience handler (timeout / retry / circuit breaker), the outbox for at-least-once…","i":"ConfigureHttpClientDefaults AddTypedServiceClient AddTypedGrpcClient MMCA.Common.Aspire"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#decision","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Decision","x":"1. Resilience is a framework invariant, not a per-call choice. Every outbound HttpClient and gRPC client registered through the framework's extension methods (AddTypedGrpcClient,…","i":"MMCA.Common.Grpc.Tests ResilienceHandlerTests AddTypedServiceClient AddTypedGrpcClient MMCA.Common.Aspire HttpClient"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#rationale","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Rationale","x":"- Invariant over discipline. A fitness function turns \"remember to add resilience\" into a build gate: the same approach the framework already uses for the layer rules and the…"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#trade-offs","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Trade-offs","x":"- The named gate (ResilienceHandlerTests, MMCA.Common.Grpc.Tests) asserts that the gRPC client path (AddTypedGrpcClient) registers the standard handler, not the runtime behavior…","i":"ResilienceCircuitBreakerFaultInjectionTests MMCA.Common.Grpc.Tests ResilienceHandlerTests AddTypedServiceClient AddTypedGrpcClient"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#revision-2026-08-18","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"This record's first Decision point scoped resilience to \"every outbound HttpClient and gRPC client registered through the framework's extension methods\". That scope was accurate…","i":"BrokerResilienceDefaults BrokenCircuitException HttpResilienceDefaults CommandTimeoutSeconds EnableRetryOnFailure ResiliencePipeline DbContextFactory OutboxProcessor HttpClient"},{"u":"/docs/adr/010-integration-event-schema-versioning.html","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#status","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-19). Updated 2026-06-27 (Helpdesk enforcement gap closed; all three consumers now gate the convention). Updated 2026-08-14 (ADC now gates seven events, and a…","i":"OutputCacheEvictionRequested"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#context","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Context","x":"Integration events cross service boundaries (Identity → Conference, Conference ↔ Engagement, …) and are resolved by consumers solely by their type string: the outbox serializes…","i":"OutboxMessage.FromDomainEvent DateOccurred EventType MessageId"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#decision","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Decision","x":"1. Every integration event carries an explicit SchemaVersion. BaseIntegrationEvent exposes public virtual int SchemaVersion = 1;. It is serialized with the payload…","i":"MMCA.Common.Testing.Architecture EventConventionTestsBase BaseIntegrationEvent IIntegrationEvent UserRegisteredV2 SchemaVersion virtual public int"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#rationale","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Rationale","x":"- A signal, enforced. A version field plus a build-gating convention test turns \"remember the contract\" into something the tooling checks: the same invariant-over-discipline…","i":"virtual"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#trade-offs","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Trade-offs","x":"- SchemaVersion is a signal, not a mechanism: by itself it does not stop a consumer breaking on a real reshape. The load-bearing half is the discipline (new type + upcaster). At…","i":"RegisterUpcastedIntegrationEventConsumer MMCA.Helpdesk.Architecture.Tests EventVersioningConventionTests ProductCreatedIntegrationEvent MMCA.Store.Architecture.Tests OutputCacheEvictionRequested TicketOpenedIntegrationEvent MMCA.ADC.Architecture.Tests OrderPlacedIntegrationEvent EventConventionTestsBase CommonArchitectureMap map.ModuleNames.Count"},{"u":"/docs/adr/011-single-locale-i18n.html","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records"},{"u":"/docs/adr/011-single-locale-i18n.html#status","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Status","x":"Superseded by ADR-027 (2026-06-27). Originally Accepted (2026-06-19). The \"if multi-locale is ever required\" scope below is the blueprint ADR-027 implements; this record is…"},{"u":"/docs/adr/011-single-locale-i18n.html#context","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Context","x":"The MMCA applications (the ADC conference app, the Store) and the MMCA.Common.UI library currently ship a single locale (en-US). The architecture rubric scores…","i":"MMCA.Common.UI"},{"u":"/docs/adr/011-single-locale-i18n.html#decision","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Decision","x":"1. Single-locale (en-US) is an explicit non-goal for now. User-facing strings are inline in markup; dates/numbers use invariant or fixed formatting where appropriate. 2. The…","i":"RequestLocalization"},{"u":"/docs/adr/011-single-locale-i18n.html#rationale","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Rationale","x":"- Recording the decision converts an implicit rubric-zero into a conscious, revisitable choice: the same posture as the single-region DR acceptance in ADR-009. - Premature i18n…"},{"u":"/docs/adr/011-single-locale-i18n.html#trade-offs","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Adding a locale later touches every view plus the formatting paths: a real but bounded effort, accepted. - Hard-coded strings make a future extraction larger; mitigated by the…"},{"u":"/docs/adr/012-grpc-host-transport.html","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records"},{"u":"/docs/adr/012-grpc-host-transport.html#status","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Status","x":"Accepted (re-verified against source 2026-08-14)."},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-06-22-store-converged-to-profile-a","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-06-22): Store converged to Profile A","x":"Store originally chose Profile B, but its cross-service gRPC failed in Azure Container Apps. With Http1AndHttp2 Kestrel + transport: 'auto' ingress on a cleartext endpoint there…","i":"IProductVariantService.ExistsAsync IUserSalesExportService HTTP_1_1_REQUIRED WithJwksDiscovery AddItemCommand Http1AndHttp2 transport identity gateway httpGet Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-07-09-adc-notification-adds-a-mixed-endpoint-profile-per-endpoint-protocols","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-07-09): ADC Notification adds a mixed-endpoint profile (per-endpoint protocols)","x":"The live-channel push pipeline (ADR-039) gave ADC's Notification service an inbound cleartext gRPC server (LiveChannelPushService.PushToChannel, called best-effort by Engagement…","i":"LiveChannelPushService.PushToChannel engagementService.WithReference services__notification__grpc__0 appsettings.Development.json additionalPortMappings notificationService Http1AndHttp2 httpGet WaitFor Http2 grpc http"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-07-25-probe-listeners-are-adcs-answer-not-tcp-probes-and-gateway-routed-jwks-is-a-local-only-rule","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-07-25): probe listeners are ADC's answer, not TCP probes; and gateway-routed JWKS is a local-only rule","x":"Two claims above were written from an earlier state of the code and no longer describe either app. 1. ADC probes never touch the traffic endpoint; TCP probes were then…","i":"HTTP_1_1_REQUIRED WithJwksDiscovery identityApp.name Program.cs tcpSocket transport identity gateway httpGet Http1 grpc"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-07-28-the-probe-listener-is-the-single-pattern-in-both-apps-no-tcp-probes-anywhere","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-07-28): the probe listener is the single pattern in both apps (no TCP probes anywhere)","x":"Store PR 55 (commit 297064bb, merged 2026-07-27) ported ADC's dedicated probe listener to Store, so the Store-only tcpSocket exception recorded in the 2026-07-25 update above is…","i":"HealthProbe__Port Http1AndHttp2 tcpSocket httpGet Http1 Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-08-07-the-probe-listener-moved-into-mmcacommon-and-notifications-grpc-endpoint-carries-a-second-service","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-08-07): the probe listener moved into MMCA.Common, and Notification's gRPC endpoint carries a second service","x":"1. One shared framework method, not a per-service file. The KestrelConfiguration.cs copies the two updates above cite no longer exist in either app. The pattern was extracted…","i":"UserNotificationExportGrpcService MMCA.ADC.Notification.Contracts services__notification__grpc__0 identityService.WithReference appsettings.Development.json HttpProtocols.Http1AndHttp2 redeclareCleartextEndpoint ConfigureEndpointDefaults KestrelConfiguration.cs additionalPortMappings ASPNETCORE_ENVIRONMENT LiveChannelGrpcService"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-08-14-stores-sales-runs-the-mixed-endpoint-profile-too-so-no-pure-profile-b-host-remains","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-08-14): Store's Sales runs the mixed-endpoint profile too, so no pure Profile B host remains","x":"Sales gained an inbound gRPC edge of its own (IUserSalesExportService, the Identity-driven data-subject export), and it resolved that the same way ADC's Notification did: not by…","i":"identityService.WithReference appsettings.Development.json UserSalesExportGrpcService AddSalesUserExportClient services__sales__grpc__0 IUserSalesExportService additionalPortMappings RequireAuthorization HealthProbe__Port Http1AndHttp2 salesService _grpc.sales"},{"u":"/docs/adr/012-grpc-host-transport.html#context","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Context","x":"Once modules were extracted into separate service hosts (ADR-008) that call each other synchronously over gRPC (ADR-007), each service's Kestrel had to serve both REST traffic…","i":"HTTP_1_1_REQUIRED Http1AndHttp2 HttpClient Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#decision","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Decision","x":"Pick one of two coherent transport profiles per app, and wire the gateway forwarder and JWKS discovery to match. Use when services must serve gRPC on cleartext (any bidirectional…","i":"builder.ConfigureEndpointsWithHealthProbe UserNotificationExportGrpcService HttpProtocols.Http1AndHttp2 ConfigureEndpointDefaults LiveChannelGrpcService HttpVersion.Version20 RequestVersionOrLower HttpProtocols.Http2 RequestVersionExact HTTP_1_1_REQUIRED WithJwksDiscovery Http1AndHttp2"},{"u":"/docs/adr/012-grpc-host-transport.html#rationale","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Rationale","x":"- The Kestrel protocol choice is the root constraint; the gateway-forward mode and the JWKS authority are downstream consequences, not independent knobs. Documenting them as a…","i":"HTTP_1_1_REQUIRED Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#trade-offs","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two profiles to keep straight. A service that gains an inbound gRPC edge must migrate from Profile B to Profile A and flip ForwardHttp2 and the JWKS wiring together, or it…","i":"appsettings.Development.json additionalPortMappings appsettings.json Http1AndHttp2 ForwardHttp2 transport http2"},{"u":"/docs/adr/012-grpc-host-transport.html#related","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Related","x":"- ADR-004 (cross-service token validation via JWKS / OIDC discovery), ADR-007 (gRPC cross-service calls), ADR-008 (monolith → services + gateway topology), ADR-039 (live-channel…"},{"u":"/docs/adr/013-result-pattern.html","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records"},{"u":"/docs/adr/013-result-pattern.html#status","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-07-21 (exception-handler chain / ProblemDetails edge contract documented)."},{"u":"/docs/adr/013-result-pattern.html#context","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Context","x":"Operations at every layer fail in expected ways: input is invalid, a domain invariant is broken, a requested entity is missing, a uniqueness conflict occurs, the caller lacks…"},{"u":"/docs/adr/013-result-pattern.html#decision","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Decision","x":"Model expected failures as values using Result / Result (MMCA.Common.Shared.Abstractions), not exceptions. - A Result is either success or failure; a failure carries one or more…","i":"OperationCanceledExceptionHandler ApiControllerBase.HandleFailure MMCA.Common.Shared.Abstractions GrpcResultExceptionInterceptor AddCommonExceptionHandlers OperationCanceledException ValidationExceptionHandler DbUpdateExceptionHandler DomainExceptionHandler GlobalExceptionHandler UnprocessableEntity ValidationException"},{"u":"/docs/adr/013-result-pattern.html#rationale","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Rationale","x":"- Failures are in the signature. A method that can fail returns Result , so the caller cannot silently ignore the failure path the way an uncaught exception allows. - Category,…","i":"Result.Failure HandleFailure ErrorType IsFailure requestId Result"},{"u":"/docs/adr/013-result-pattern.html#trade-offs","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Trade-offs","x":"- More ceremony at call sites than letting an exception bubble; the combinators absorb most of it. - Two error channels coexist (Result for expected, exceptions for exceptional).…","i":"GlobalExceptionHandler ErrorType Result"},{"u":"/docs/adr/013-result-pattern.html#related","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Related","x":"ADR-007 (Result over the wire via gRPC), ADR-014 (the decorator pipeline returns Result.Failure to short-circuit a command before it reaches the handler).","i":"Result.Failure"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#status","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-07-19 (Transactional semantics: rollback on business failure + post-commit event dispatch; see Revision below). Revised 2026-08-18 (the pipeline order…"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#context","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Context","x":"Commands and queries share cross-cutting concerns: validation, transactions, cache invalidation, logging / timing, and feature gating. Putting that logic inside each handler…"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#decision","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Decision","x":"Use single-responsibility handlers behind a Scrutor-composed decorator pipeline. - ICommandHandler and IQueryHandler (MMCA.Common.Application) are one handler per use case, each…","i":"ModuleLoader.DiscoverAndRegister ScanModuleApplicationServices ProfilingCommandDecorator AddApplicationDecorators AddApplicationProfiling MMCA.Common.Application ProfilingQueryDecorator ICacheInvalidating AddInfrastructure ICommandHandler IQueryCacheable AddApplication"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#rationale","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Rationale","x":"- Thin, testable handlers. A handler has no transaction, logging, or caching plumbing, so it is unit-tested in isolation. - One place to read and change the pipeline. The order…"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#trade-offs","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Trade-offs","x":"- Registration order is the reverse of execution order (a Scrutor foot-gun), mitigated by the inline ordering comments in AddApplicationDecorators(). - Decorators must be…","i":"AddApplicationDecorators"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#revision-2026-07-19","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Revision (2026-07-19)","x":"Two Transactional-decorator semantics changed with the 2026-07-19 full review: - A returned business failure now rolls the transaction back. Previously a handler returning…","i":"DbContextFactory.ExecuteInTransactionAsync DomainEventSaveChangesInterceptor RollbackTransaction Result.Failure IsFailure Result"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#revision-2026-08-18","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Two decorators were added to both chains, so the order recorded in the Decision above is no longer the shipped one. The registration site is unchanged in kind:…","i":"CancellationTokenSource.CreateLinkedTokenSource cqrs.authorization.denied.count DecoratorPipelineOrderTestsBase AuthorizationCommandDecorator AddApplicationDecorators AuthorizationDenied ICurrentUserService IPermissionRegistry IRequiresPermission budget.CancelAfter cqrs.timeout.count hasTimeout.Timeout"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#related","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Related","x":"ADR-013 (Result, the short-circuit currency of the pipeline, and the Failure error type the timeout decorator reuses because the taxonomy has no timeout member), ADR-003…","i":"IPermissionRegistry MMCA.Common.Cqrs HasPermission SaveChanges Failure"},{"u":"/docs/adr/015-architecture-fitness-functions.html","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records"},{"u":"/docs/adr/015-architecture-fitness-functions.html#status","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-18 (two new rule families, namespace dependency cycles and trailing CancellationToken declarations, plus a third enforcement layer: a compile-time…","i":"CancellationToken proto"},{"u":"/docs/adr/015-architecture-fitness-functions.html#context","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Context","x":"The codebase rests on invariants that are easy to state and easy to erode by accident: clean- architecture layer flow (Domain depends on nothing above it), module isolation (no…","i":"SchemaVersion"},{"u":"/docs/adr/015-architecture-fitness-functions.html#decision","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Decision","x":"Enforce architectural invariants as automated checks that gate the build, in two layers (a third joined them on 2026-08-18: see the Revision at the end). 1. Compile-time guard.…","i":"MMCA.Common.LayerEnforcement.targets MMCA.Common.Testing.Architecture HelpdeskArchitectureMap CommonArchitectureMap StoreArchitectureMap AdcArchitectureMap IArchitectureMap ProjectReference dotnet test"},{"u":"/docs/adr/015-architecture-fitness-functions.html#rationale","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Rationale","x":"- Invariant over discipline. Turning \"do not do X\" into a red build is the only enforcement that scales. It is the same lever used by the layer rules, the resilience gate…","i":"IArchitectureMap"},{"u":"/docs/adr/015-architecture-fitness-functions.html#trade-offs","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Trade-offs","x":"- The tests assert structure / registration, not runtime behavior. ADR-009's test proves a client wires resilience, not that its policy values are correct; parameter tuning stays…","i":"FrameworkSanityTests IArchitectureMap"},{"u":"/docs/adr/015-architecture-fitness-functions.html#revision-2026-08-18","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Two new rule families joined the shared library, and a third enforcement layer joined the two the Decision above describes. The counts in MMCA.Common/FACTS.md move with them: 102…","i":"AsyncMethodsDeclareTrailingCancellationToken Microsoft.CodeAnalysis.PublicApiAnalyzers ArchitectureRules.CancellationTokens dotnet_analyzer_diagnostic.severity NamespacesHaveNoDependencyCycles MMCA.Common.Infrastructure Context.ConnectionAborted IHostedService.StartAsync ArchitectureRules.Cycles TenancySettingsValidator InternalAPI.Shipped.txt NamespaceCycleTestsBase"},{"u":"/docs/adr/015-architecture-fitness-functions.html#revision-2026-08-18-section-b-rule-families","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Revision (2026-08-18): Section B rule families","x":"A second entry on the same date, kept separate rather than folded into the one above because it lands with a different wave and changes a number that revision states. Two rule…","i":"IdempotencyConventionTestsBase ArchitectureRules.Protos ProtoContractTestsBase PublicAPI.Shipped.txt FrozenProtoContracts csharp_namespace SolutionFileName FactsGenerator justification NonIdempotent Idempotent ProtoFiles"},{"u":"/docs/adr/015-architecture-fitness-functions.html#related","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Related","x":"ADR-009 (resilience gate), ADR-010 (event-version gate), ADR-016 (MassTransit pin gate, and the lockstep release cadence the public API baseline is pinned to), ADR-006/007/008…","i":"CancellationToken"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#status","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Amended (2026-07-28): the fitness function now gates two commercial-license majors (MassTransit and SixLabors.ImageSharp), so the decision is restated as…","i":"MassTransit.Azure.ServiceBus.Core Directory.Packages.props SixLabors.ImageSharp"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#context","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common publishes its MMCA.Common. NuGet package set (see FACTS.md for the authoritative list and count) consumed by three downstream repos: the two production apps (Store,…","i":"Directory.Packages.props Infrastructure MassTransit MT_LICENSE FACTS.md Domain"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#decision","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Decision","x":"1. Version the whole MMCA.Common. package set in lockstep. All packages share one version (MinVer, derived from a single vX.Y.Z git tag); a release tags every package (see…","i":"MassTransit.Azure.ServiceBus.Core RestorePackagesWithLockFile DependencyVersionTestsBase MMCA.Common.Infrastructure Directory.Packages.props MassTransit.RabbitMQ SixLabors.ImageSharp MassTransit MT_LICENSE FACTS.md Obsolete vX.Y.Z"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#rationale","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Rationale","x":"- One version, one compatibility story. Lockstep removes the N-package matrix: \"everything on vX.Y.Z\" is the only supported combination, which is the right trade for a small…","i":"vX.Y.Z"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#trade-offs","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Trade-offs","x":"- A consumer cannot adopt a single package in isolation: it takes the whole set at the new version. - Lockstep will bump a package whose code did not change (acceptable: the…","i":"MassTransit.Azure.ServiceBus.Core Directory.Packages.props dependabot.yml"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#related","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (the fitness function that enforces the pins), ADR-003 / ADR-006 (MassTransit is the broker transport behind the outbox and database-per-service flows)."},{"u":"/docs/adr/017-request-idempotency.html","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records"},{"u":"/docs/adr/017-request-idempotency.html#status","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-01: the guard around execute-and-store is now an IDistributedLock resolved from DI (Redis-backed wherever a connection multiplexer is registered, which…","i":"IdempotencyConventionTestsBase IDistributedLock justification NonIdempotent ObjectResult Idempotent NoContent HttpPost"},{"u":"/docs/adr/017-request-idempotency.html#context","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Context","x":"Write endpoints (POST / PUT / PATCH) are exposed to client retries and double-submits: a flaky network, an impatient user double-clicking, or a resilience pipeline re-issuing a…","i":"Result"},{"u":"/docs/adr/017-request-idempotency.html#decision","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Decision","x":"Provide opt-in, client-driven request idempotency as an MVC action filter in MMCA.Common.API. - Opt-in per action. [Idempotent] (IdempotentAttribute, a ServiceFilterAttribute…","i":"IdempotencySettings.CacheExpirationHours InProcessDistributedLock IConnectionMultiplexer ServiceFilterAttribute KeyedSemaphoreStripe RedisDistributedLock IdempotentAttribute AddInfrastructure IdempotencyFilter IDistributedLock StatusCodeResult MMCA.Common.API"},{"u":"/docs/adr/017-request-idempotency.html#rationale","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Rationale","x":"- Safety at the edge, not in every handler. Deduplication lives in one filter, so a handler stays a thin use case (ADR-014) and does not grow ad-hoc \"did I already do this?\"…","i":"Idempotent"},{"u":"/docs/adr/017-request-idempotency.html#trade-offs","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Trade-offs","x":"- Cross-instance mutual exclusion follows Redis, so it is a deployment property, not a guarantee. Every ADC and Store service host registers a Redis IConnectionMultiplexer when a…","i":"IConnectionMultiplexer StatusCodeResult IAnonymizable ObjectResult Idempotent Location redis"},{"u":"/docs/adr/017-request-idempotency.html#revision-2026-08-18","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"The last Trade-off above is the one this revision addresses, and it does so by changing what is required. Nothing here makes an endpoint idempotent. What it requires is that…","i":"PostActions_ShouldDeclare_IdempotencyIntent IdempotencyConventionTestsBase AttributeTargets.Method NonIdempotentAttribute GetCustomAttributes AuthControllerBase EnableRateLimiting MMCA.Common.API AttributeUsage justification Justification NonIdempotent"},{"u":"/docs/adr/017-request-idempotency.html#related","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (handler idempotency for outbox/event consumers, a distinct concern), ADR-013 (Result is the response the filter caches/replays), ADR-014 (the filter keeps the handler…","i":"ICacheService NonIdempotent"},{"u":"/docs/adr/018-polyglot-persistence.html","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records"},{"u":"/docs/adr/018-polyglot-persistence.html#status","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Status","x":"Accepted. The framework plumbing is complete, covered by unit and integration tests (DataSourceResolverTests, CrossDataSourceDegradeConventionTests, EntityTypeConfigurationTests,…","i":"CrossDataSourceDegradeConventionTests CosmosConfigurationPortabilityTests MultiSourceSqliteIntegrationTests EntityTypeConfigurationTests DataSourceResolverTests FACTS.md Session Room"},{"u":"/docs/adr/018-polyglot-persistence.html#context","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Context","x":"ADR-006 (database-per-service) splits storage along the Name axis: several physically separate databases, all on the same engine (SQL Server), one per service. A second,…","i":"DataSourceKey Engine Name"},{"u":"/docs/adr/018-polyglot-persistence.html#decision","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Decision","x":"Support three storage engines behind one entity model and one set of repository abstractions, selected per entity configuration. 1. DataSource engine enum: SQLServer (full…","i":"CrossDataSourceDegradeConvention EntityTypeConfigurationSQLServer EntityTypeConfigurationCosmos EntityTypeConfigurationSqlite SQLServerMigrationsAssembly CosmosIntIdValueGenerator SQLServerConnectionString EntityDataSourceRegistry EntityTypeConfiguration CosmosConnectionString SqliteConnectionString ApplicationDbContext"},{"u":"/docs/adr/018-polyglot-persistence.html#rationale","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Rationale","x":"- Right store per access pattern, as a configuration decision. The engine becomes an attribute on a configuration class, not a rewrite. The same domain entity, application…"},{"u":"/docs/adr/018-polyglot-persistence.html#trade-offs","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Trade-offs","x":"- No cross-engine JOINs, FKs, or transactions. This is the ADR-006 cost made sharper: across engines it is a hard limit, not a deployment choice. A query spanning engines (for…","i":"SpecificationsDoNotNavigateToOtherEntities CrossSourceSpecification specifications keys"},{"u":"/docs/adr/018-polyglot-persistence.html#related","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Related","x":"ADR-006 (database-per-service: the Name axis this ADR's Engine axis is orthogonal to; they share DataSourceKey), ADR-002 (navigation populators bridge the relationships the…","i":"DataSourceKey"},{"u":"/docs/adr/019-rate-limiting.html","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records"},{"u":"/docs/adr/019-rate-limiting.html#status","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-01 (the auth-ip per-IP anonymous-authentication limiter, which the shared auth controller applies to login and register by default, is recorded as the…","i":"RateLimitingSettings UserPolicy"},{"u":"/docs/adr/019-rate-limiting.html#context","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Context","x":"Every service exposes read and write endpoints to the public internet through the gateway (ADR-008). Abusive or runaway clients (scrapers, credential stuffing, retry storms, a…"},{"u":"/docs/adr/019-rate-limiting.html#decision","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Decision","x":"Rate limiting is layered, and the always-on global limiter is authenticated-only. 1. A global limiter that only caps authenticated callers. AddCommonRateLimiting…","i":"HttpContext.Connection.RemoteIpAddress EnableRateLimitingAttribute UseCommonMiddlewarePipeline AttributeUsage.Inherited LoginProtectionService AddCommonRateLimiting RateLimitPolicyAuthIp GetCustomAttributes UseForwardedHeaders AuthControllerBase EnableRateLimiting EndpointDataSource"},{"u":"/docs/adr/019-rate-limiting.html#rationale","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Rationale","x":"- Limit the traffic that is both attributable and expensive. An authenticated request is tied to a principal and usually drives the database; capping per-principal stops a single…"},{"u":"/docs/adr/019-rate-limiting.html#trade-offs","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Trade-offs","x":"- The global limiter only protects the authenticated surface. The anonymous surface is covered endpoint by endpoint instead: login and register carry the auth-ip limiter by…","i":"ForwardLimit"},{"u":"/docs/adr/019-rate-limiting.html#revision-2026-08-18","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"The layering above is unchanged: the global limiter is still authenticated-only, infrastructure and anonymous traffic are still exempt, auth-ip still covers login and register by…","i":"RateLimitAlgorithm.FixedWindow RedisFixedWindowRateLimiter IConnectionMultiplexer Interlocked.Exchange RateLimitingSettings StringIncrementAsync PerUserPermitLimit AuthIpPermitLimit GlobalPermitLimit SegmentsPerWindow allowDistributed SlidingWindow"},{"u":"/docs/adr/019-rate-limiting.html#related","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the JWKS/discovery traffic the limiter exempts, and the authenticated principal it keys on), ADR-008 (the gateway edge this protects), ADR-017 (request idempotency, the…","i":"RateLimitingSettings IncrementAsync UseRateLimiter Distributed INCR"},{"u":"/docs/adr/020-permission-based-authorization.html","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records"},{"u":"/docs/adr/020-permission-based-authorization.html#status","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-25, amended 2026-07-10)."},{"u":"/docs/adr/020-permission-based-authorization.html#context","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Context","x":"Authorization started as pure role-based access control (RBAC). Endpoints declared the role they required with [Authorize(Policy = ...)] against named policies: RequireOrganizer,…","i":"RequireAuthenticatedUser RequireAuthenticated RequireOrganizer RequireAttendee RequireAdmin RequireRole Authorize Policy"},{"u":"/docs/adr/020-permission-based-authorization.html#decision","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Decision","x":"Add a permission (capability) layer over RBAC, opt-in and backward-compatible. - A central registry maps roles to permissions. IPermissionRegistry / PermissionRegistry…","i":"DefaultAuthorizationPolicyProvider PermissionAuthorizationHandler AuthClaimTypes.Permission PermissionRegistryBuilder AddAuthorizationPolicies PermissionPolicyProvider RequireAuthenticatedUser MMCA.Common.Shared.Auth RoleNames.ContentEditor HasPermissionAttribute PermissionRequirement IPermissionRegistry"},{"u":"/docs/adr/020-permission-based-authorization.html#rationale","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Rationale","x":"- Capabilities decouple endpoints from roles. A route says what it does (conference:sessions:manage), and who may do it is a registry decision, so adding ContentEditor with a…","i":"ContentEditor"},{"u":"/docs/adr/020-permission-based-authorization.html#trade-offs","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Trade-offs","x":"- It is still RBAC, not ABAC. The model resolves role to permission; it does not evaluate resource or attribute conditions. Per-resource ownership (\"a customer may read only…","i":"ConferencePermissions OwnerOrAdminFilter AddPermissions IAnonymizable Idempotent Grant"},{"u":"/docs/adr/020-permission-based-authorization.html#related","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the authenticated principal and claims this keys on, including the optional permission claim), ADR-008 (each extracted service authorizes independently, so the registry…","i":"permission"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#status","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-09; adoption reviewed 2026-07-15). Revised 2026-08-18 (the inbox stays opt-in, but being off is no longer silent: a broker-connected host running NoOpInboxStore…","i":"NoOpInboxStore InboxMessages"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#context","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Context","x":"ADR-003 makes integration-event delivery at-least-once: the outbox guarantees a published event is not lost, and the MassTransit broker redelivers on consumer failure.…"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#decision","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Decision","x":"Add an opt-in inbox that records each successfully-processed integration event by its MessageId and skips redeliveries. - Every event carries a MessageId. BaseDomainEvent stamps…","i":"IX_InboxMessages_MessageId IntegrationEventConsumer SessionFeedbackSubmitted SpeakerUnlinkedFromUser EventFeedbackSubmitted AlreadyProcessedAsync ProductVariantChanged OutboxCleanupService SpeakerLinkedToUser AddBrokerMessaging MarkProcessedAsync AttendeeCheckedIn"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#rationale","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Rationale","x":"- Dedup once, not in every handler. A single consume-edge check turns \"every handler author must remember to be idempotent against redelivery\" into a framework guarantee for the…","i":"NoOpInboxStore"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#trade-offs","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Trade-offs","x":"- Not exactly-once. The crash-after-handler-before-inbox window reprocesses once, so handlers must stay idempotent for it; the inbox narrows the duplicate window, it does not…","i":"InboxMessages EnableInbox MessageId"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#related","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox and at-least-once delivery whose consumer side this deduplicates; handler idempotency is still required for the crash window), ADR-006 (the inbox lives in the…","i":"OutboxCleanupService InProcess"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#revision-2026-08-18","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"The decision is unchanged: the inbox is still opt-in and NoOpInboxStore is still the default. What changed is that the default is now loud. 1. A broker-connected host with no…","i":"ApplicationDbContext.OnModelCreating MessageBusProvider.InProcess InboxDisabledWarningService IX_InboxMessages_MessageId IEntityTypeConfiguration base.OnModelCreating AddBrokerMessaging SQLServerDbContext AddInboxMessages CosmosDbContext SqliteDbContext ConfigureInbox"},{"u":"/docs/adr/022-browser-session-cookie-auth.html","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#status","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Status","x":"Accepted."},{"u":"/docs/adr/022-browser-session-cookie-auth.html#context","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Context","x":"The apps are Blazor Web Apps: a server-rendered (SSR) prerender pass runs on the first request, then an interactive phase (Blazor Server or WebAssembly) takes over.…","i":"Authorization localStorage Authorize"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#decision","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Decision","x":"Carry the session in HttpOnly cookies and add an authentication scheme that reads them during SSR prerender. The mechanism ships in MMCA.Common.API (SessionCookies/) with a…","i":"SessionCookieAuthenticationHandler CookieSessionRefresher mmca_auth_refresh HttpContext.User mmca_auth_access SessionCookieJar MMCA.Common.API MMCA.Common.UI Authorize DELETE POST"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#rationale","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Rationale","x":"- Fixes the fresh-GET prerender gap. Without a server-readable session, every deep-link or F5 to an [Authorize] page would redirect to /login despite a valid session; the cookie…","i":"Authorize"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#trade-offs","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Trade-offs","x":"- A non-validating auth scheme exists. SessionCookieAuthenticationHandler trusts a cookie it does not cryptographically verify. This is sound only because (a) the cookie is…","i":"SessionCookieAuthenticationHandler ISessionCookieSync"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#related","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the JWT/JWKS validation the API performs on every call, which is why the SSR handler can skip signature validation), ADR-008 (the gateway and topology the UI talks to),…"},{"u":"/docs/adr/023-security-response-headers.html","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records"},{"u":"/docs/adr/023-security-response-headers.html#status","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02)."},{"u":"/docs/adr/023-security-response-headers.html#context","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Context","x":"Every client-facing host (the YARP Gateway and the Blazor UI web host in each app) must stamp the same hardened HTTP response headers: X-Content-Type-Options, X-Frame-Options,…"},{"u":"/docs/adr/023-security-response-headers.html#decision","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Decision","x":"Ship one security-headers middleware in MMCA.Common.Aspire (MMCA.Common.Aspire.Security), registered with AddCommonSecurityHeaders(configuration?, configure?) and inserted early…","i":"SecurityHeadersSettings.ContentSecurityPolicy SecurityHeadersMiddlewareTests MMCA.Common.Aspire.Security SecurityHeadersMiddleware AddCommonSecurityHeaders MMCA.Common.Aspire.Tests UseCommonSecurityHeaders BlazorCspPolicyProvider SecurityHeadersSettings StaticCspPolicyProvider AddCommonBlazorCsp ICspPolicyProvider"},{"u":"/docs/adr/023-security-response-headers.html#rationale","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Rationale","x":"- One hardened default, defined once. Centralizing the header set removes per-host drift and makes a new edge host secure by default rather than by remembering to copy headers. -…","i":"ICspPolicyProvider"},{"u":"/docs/adr/023-security-response-headers.html#trade-offs","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Trade-offs","x":"- The baseline CSP is intentionally incomplete. An API/Gateway host gets default-src 'self'-style protection but no script-src/style-src discipline unless it registers a fuller…","i":"SecurityHeadersSettings.ContentSecurityPolicy AddCommonSecurityHeaders BlazorCspPolicyProvider AddCommonBlazorCsp ICspPolicyProvider MMCA.Common.UI.Web TryAddSingleton ApiSettings"},{"u":"/docs/adr/023-security-response-headers.html#related","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Related","x":"ADR-019 (rate limiting, the other always-on edge protection living in the same Aspire layer), ADR-022 (browser session-cookie auth, the other browser-edge security control),…"},{"u":"/docs/adr/024-push-notifications.html","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records"},{"u":"/docs/adr/024-push-notifications.html#status","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27, amended 2026-07-15). Revised 2026-08-07 (transactional email recorded as an app-level concern outside the channel model; see Revision below). Revised…","i":"Enabled"},{"u":"/docs/adr/024-push-notifications.html#context","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Context","x":"The framework needs to deliver user-facing notifications (an organizer broadcasting a schedule change, a per-user alert). Two delivery models each fail on their own. A pure…"},{"u":"/docs/adr/024-push-notifications.html#decision","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Decision","x":"Deliver notifications over two channels from one application use case, with the transport and the recipient policy both behind abstractions. - A durable per-user inbox plus a…","i":"NullNotificationRecipientProvider PushNotificationSettings.Enabled INotificationRecipientProvider SignalRPushNotificationSender SendPushNotificationHandler SignalRLiveChannelPublisher MMCA.Common.Infrastructure NullPushNotificationSender IPushNotificationSender MMCA.Common.Application CancellationToken.None NotificationHubService"},{"u":"/docs/adr/024-push-notifications.html#rationale","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Rationale","x":"- Each channel covers the other's failure mode. The inbox guarantees eventual delivery to offline users; the push gives connected users immediacy. Persisting the inbox before…","i":"INotificationRecipientProvider IPushNotificationSender IMessageBus"},{"u":"/docs/adr/024-push-notifications.html#trade-offs","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Fan-out write amplification. One UserNotification row is written per recipient, so a broadcast to a large audience is a large insert. This is fine for the current per-event /…","i":"NullPushNotificationSender AddPushNotifications PushNotification UserNotification Authorization access_token IsRead ReadOn"},{"u":"/docs/adr/024-push-notifications.html#related","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox dual-dispatch path, which is distinct: that carries service-to-service integration events, this carries user-facing notifications), ADR-004 (the /hubs…","i":"MMCA.ADC.Notification.Service SendPushNotificationHandler NullNativePushSender Http1AndHttp2 access_token Http2"},{"u":"/docs/adr/024-push-notifications.html#revision-2026-08-07","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Records transactional email, a delivery path the channel model above never mentions. The decision is unchanged: this closes a documentation gap so the asymmetry reads as…","i":"OrderPaymentFailedSagaHandler SendPushNotificationHandler IPushNotificationSender ILiveChannelPublisher IPushDeviceRegistrar IDomainEventHandler AddInfrastructure INativePushSender OrderPaidHandler PushNotification UserNotification SmtpEmailSender"},{"u":"/docs/adr/025-startup-warmup-readiness.html","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records"},{"u":"/docs/adr/025-startup-warmup-readiness.html#status","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27). Revised 2026-07-28: /health/ready now excludes optional-tagged checks as well as live-tagged ones (see Decision), and the absence of a warm-up timeout was…","i":"optional live"},{"u":"/docs/adr/025-startup-warmup-readiness.html#context","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Context","x":"On the Azure Container Apps Consumption plan a replica that has been idle is CPU-throttled, and a scale-from-zero or scaled-out replica starts cold. The first authenticated…"},{"u":"/docs/adr/025-startup-warmup-readiness.html#decision","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Decision","x":"Ship a small warm-up subsystem in MMCA.Common.Aspire, wired into AddServiceDefaults() so every host gets it. - A readiness gate that starts closed. WarmupReadinessGate…","i":"OpenIdConnectMetadataWarmupTask OperationCanceledException WarmupReadinessHealthCheck MapDefaultEndpoints WarmupHostedService WarmupReadinessGate AddServiceDefaults AddWarmupReadiness IHttpClientFactory MMCA.Common.Aspire TaskTimeoutSeconds cancellationToken"},{"u":"/docs/adr/025-startup-warmup-readiness.html#rationale","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Rationale","x":"- Keep cold replicas out of rotation, briefly. Gating readiness on warm-up means the platform does not send a user request to a replica that is still doing its first handshakes,…","i":"AddServiceDefaults"},{"u":"/docs/adr/025-startup-warmup-readiness.html#trade-offs","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Trade-offs","x":"- A replica can enter rotation not fully warm. The gate is opened in a finally once the Task.WhenAll over every registered task returns, that is, once each task has completed,…","i":"ConfigurationManager TimeoutException stoppingToken Task.WhenAll WaitAsync finally"},{"u":"/docs/adr/025-startup-warmup-readiness.html#related","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the OIDC discovery document the built-in task pre-fetches, and the auth-side view of the same cold-start), ADR-009 (the Polly resilience pipeline that absorbs the lazy…"},{"u":"/docs/adr/026-caching-strategy.html","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","i":"ICacheService"},{"u":"/docs/adr/026-caching-strategy.html#status","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27, amended 2026-07-10, 2026-07-23, 2026-07-25, 2026-08-14). Amended by ADR-077 (2026-08-13): Tier 1's substrate gains a third, opt-in implementation…","i":"OutputCacheEvictionRequested MMCA.Common.OutputCache HybridCacheService"},{"u":"/docs/adr/026-caching-strategy.html#context","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Context","x":"The framework needs caching in two distinct places. Inside the application pipeline, query results are memoized and invalidated on mutation (the Caching decorators of ADR-014,…","i":"ICacheInvalidating IQueryCacheable"},{"u":"/docs/adr/026-caching-strategy.html#decision","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Decision","x":"Cache in two tiers, each with its own substrate. - One abstraction. ICacheService (MMCA.Common.Application/Interfaces/ICacheService.cs) exposes GetAsync / SetAsync / RemoveAsync…","i":"builder.Services.AddStackExchangeRedisOutputCache OutputCacheOptions.AddPublicEndpointPolicy AbsoluteExpirationRelativeToNow PublicEndpointOutputCachePolicy CacheOptions.DefaultExpiration CacheOptions.DefaultDuration DistributedCacheEntryOptions DistributedCacheService IConnectionMultiplexer LoginProtectionService MemoryDistributedCache AddCommonHybridCache"},{"u":"/docs/adr/026-caching-strategy.html#rationale","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Rationale","x":"- One substrate, swapped by environment. Keeping ICacheService as the only thing application code sees lets the deployment decide memory vs distributed. The auto-swap (presence…","i":"ICacheInvalidating IDistributedCache ICacheService"},{"u":"/docs/adr/026-caching-strategy.html#trade-offs","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Trade-offs","x":"- Memory mode is per-replica. In the in-process store each replica caches independently; a scaled-out deployment that did not wire Redis would see cross-replica staleness bounded…","i":"ICacheService.IncrementAsync AddRedisDistributedCache DistributedCacheService StackExchangeRedisCache IConnectionMultiplexer AddOutputCache AddRedisClient RemoveAsync WRONGTYPE NoCache absexp sldexp"},{"u":"/docs/adr/026-caching-strategy.html#related","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Related","x":"ADR-014 (the Caching decorators and IQueryCacheable / ICacheInvalidating markers that consume this substrate), ADR-019 (output caching as the anonymous-traffic lever, and…","i":"LoginProtectionService HybridCacheService ICacheInvalidating IQueryCacheable IncrementAsync ICacheService WRONGTYPE"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-07-24","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Three substrate corrections from a code review. 1. An optional key namespace (Cache:KeyPrefix). Services sharing one cache instance also share one keyspace, and nothing stopped…","i":"RedisCacheOptions.InstanceName ICacheService.IncrementAsync DistributedCacheService EvictionReason.Replaced KeyedSemaphoreStripe RemoveByPrefixAsync MemoryCacheService IMemoryCache InstanceName CacheKey INCR"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-07-25","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"An audit against the code. No behavior changed; the ADR text did. 1. The IncrementAsync entry above was wrong. It described a Redis INCR override. There is no such override, and…","i":"DistributedCacheService StackExchangeRedisCache IncrementAsync AddCaching INCR"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-07-28","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-07-28)","x":"Line anchors only, re-verified against the current source. No decision, no behavior, and no substantive prose changed. 1. Tier 2. Store Catalog's…","i":"AddStackExchangeRedisOutputCache DistributedCacheService MMCA.Common.API ICacheService AddCaching"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-01","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"Line anchors only, re-verified against the current source. No decision, no behavior, and no substantive prose changed. 1. AddCaching. Now at…","i":"AddStackExchangeRedisOutputCache DistributedCacheService RemoveByPrefixAsync ScanAndDeleteAsync IncrementAsync AddCaching remarks returns"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-07","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Line anchors only, re-verified against the current source. No decision, no behavior, and no substantive prose changed. 1. AddCaching. Now at…","i":"AddStackExchangeRedisOutputCache DistributedCacheService MemoryDistributedCache TimeSpan.FromSeconds MemoryCacheService IDistributedCache MMCA.Common.API CacheOptions AddCaching"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-13","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-13)","x":"Tier 1 is amended by ADR-077, which is where the decision and its trade-offs are recorded. The three points that change the reading of this record: 1. A third substrate, opted…","i":"Microsoft.Extensions.Caching.Hybrid DistributedCacheService StackExchangeRedisCache AddCommonHybridCache HybridCacheService MemoryCacheService IDistributedCache IncrementAsync AddCaching WRONGTYPE prefix INCR"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-14","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-14)","x":"One substrate correction plus a line-anchor re-verification. No decision and no behavior changed. 1. The 30-second default now has a named home, CacheOptions.DefaultDuration.…","i":"AddStackExchangeRedisOutputCache AbsoluteExpirationRelativeToNow CacheOptions.DefaultDuration DistributedCacheEntryOptions AddRedisDistributedCache DistributedCacheService HybridCacheEntryOptions TimeSpan.FromSeconds app.UseOutputCache HybridCacheService DefaultExpiration DefaultDuration"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-18","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Every previous revision moved Tier 1. This one moves Tier 2, and it is the first change to the output-cache edge since ADR-040. Tier 2 as decided here is per-process by…","i":"RegisterOutputCacheEvictionConsumer RegisterIntegrationEventConsumer AddOutputCacheEvictionHandler OutputCacheEvictionRequested OperationCanceledException OutputCacheEvictionHandler BestEffort.ExecuteAsync MMCA.Common.OutputCache MMCA.Common.BestEffort cache.eviction.failed registerFaultConsumer BaseIntegrationEvent"},{"u":"/docs/adr/027-multi-locale-i18n.html","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records"},{"u":"/docs/adr/027-multi-locale-i18n.html#status","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27, amended 2026-07-02, 2026-07-03, 2026-07-09, and 2026-07-29; corrected 2026-08-01: the pseudo-locale CI gate is required on all three browser engines, and…"},{"u":"/docs/adr/027-multi-locale-i18n.html#context","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Context","x":"ADR-011 recorded single-locale (en-US) as a deliberate, revisitable non-goal and sketched what re-introducing i18n would entail. That revisit has now happened: the framework adds…","i":"InteractiveAuto Error Code"},{"u":"/docs/adr/027-multi-locale-i18n.html#decision","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Decision","x":"1. Supported cultures are an explicit allowlist: en-US (default) + es. Adding a locale is adding a .es.resx sibling set and one allowlist entry, not new infrastructure. 2.…","i":"MmcaCultureBootstrap.SetBrowserCultureAsync ErrorHttpMapping.BuildErrorsExtension DomainInvariantViolationException CultureInfo.DefaultThreadCurrent LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture SupportedCultures.ResolveClosest ApiControllerBase.HandleFailure ResourceTranslationsAreComplete SupportedCultures.PseudoLocale CookieRequestCultureProvider CultureInfo.InvariantCulture"},{"u":"/docs/adr/027-multi-locale-i18n.html#rationale","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Rationale","x":"- Keying error localization on the existing Error.Code is the cheapest correct extension point. The codes are already stable and already cross the wire; localizing at the edge…","i":"ResourcesPath Error.Code resx"},{"u":"/docs/adr/027-multi-locale-i18n.html#trade-offs","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Every view and every user-facing message is touched: a large, mostly mechanical sweep, accepted as the cost ADR-011 always named. - WASM Spanish formatting needs ICU…","i":"InvariantGlobalization ResxMudLocalizer MudTranslations BlazorWebView MudLocalizer"},{"u":"/docs/adr/027-multi-locale-i18n.html#related","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Related","x":"ADR-011 (superseded), ADR-013 (the Error.Code this localizes on), ADR-015 (the i18n gates now live here: the MA0076 culture-less formatting build gate and the…","i":"ResourceTranslationsAreComplete BlazorWebView Error.Code MA0076"},{"u":"/docs/adr/028-dark-theme-mode.html","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records"},{"u":"/docs/adr/028-dark-theme-mode.html#status","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27; revised 2026-07-15)."},{"u":"/docs/adr/028-dark-theme-mode.html#context","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Context","x":"MMCATheme (MMCA.Common.UI/Theme/MMCATheme.cs) has always defined a complete, brand-tuned PaletteDark alongside PaletteLight, but MudThemeProvider was hard-wired to light: no…","i":"MudThemeProvider InteractiveAuto PaletteLight PaletteDark IsDarkMode MMCATheme ref"},{"u":"/docs/adr/028-dark-theme-mode.html#decision","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Decision","x":"1. Bind the existing theme. The shared MainLayout renders a single component (MMCA.Common.UI/Layout/MainLayout.razor:14), which owns the four Mud providers plus the Day/Dark…","i":"ThemeService.InitializeAsync User.PreferredCulture User.PreferredTheme MMCATheme.Instance MmcaThemeProviders OnAfterRenderAsync InteractiveServer systemPrefersDark window.matchMedia MudThemeProvider MMCA.Common.UI ThemeService"},{"u":"/docs/adr/028-dark-theme-mode.html#rationale","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Rationale","x":"- Reusing the i18n cookie/profile machinery means one persistence model for both user preferences, instead of two subtly different ones. Theme and locale are the same shape of…","i":"BrandColorTokenTests"},{"u":"/docs/adr/028-dark-theme-mode.html#trade-offs","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Trade-offs","x":"- The same FOUC hazard as locale is not yet closed for theme. The SSR data-theme/inline-script read is unimplemented (Decision 3), so the first paint can briefly flash the wrong…","i":"MainLayout User"},{"u":"/docs/adr/028-dark-theme-mode.html#related","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Related","x":"ADR-027 (shares the cookie source-of-truth and the User preference migration, and is the model for the theme no-flash SSR bootstrap that is not yet wired), ADR-022 (the SSR…","i":"User"},{"u":"/docs/adr/029-authentication-brute-force-protection.html","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#status","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27). Updated 2026-07-02 (the check/increment/reset call sequence was hoisted into AuthenticationServiceBase ; the adoption note and the \"convention the consumer…","i":"AuthenticationServiceBase TUser"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#context","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Context","x":"ADR-019's global rate limiter is authenticated-only: it caps requests per authenticated principal and deliberately exempts anonymous traffic. The highest-value anonymous attack…","i":"RateLimitPolicyAuthIp AuthControllerBase"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#decision","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Decision","x":"Provide a framework ILoginProtectionService (MMCA.Common.Application.Auth) with a single implementation LoginProtectionService (MMCA.Common.Infrastructure.Auth), registered…","i":"RegistrationRateLimitWindowMinutes CheckRegistrationRateLimitAsync IncrementRegistrationCountAsync MMCA.Common.Infrastructure.Auth ICacheService.IncrementAsync IncrementFailedAttemptsAsync MaxRegistrationsPerIpPerHour MMCA.Common.Application.Auth FailedAttemptWindowMinutes AuthenticationServiceBase ResetFailedAttemptsAsync DistributedCacheService"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#rationale","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Rationale","x":"- Complements ADR-019 rather than duplicating it. ADR-019 carries two limiter layers and this is the third on top of them: its global limiter caps authenticated throughput per…","i":"Result"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#trade-offs","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Trade-offs","x":"- Cache-scoped state weakens under scale-out without Redis. In memory mode the counters are per-replica and evaporate on restart, so a multi-replica deployment that did not wire…","i":"AuthenticationServiceBase ILoginProtectionService AuthenticationService TUser"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#related","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Related","x":"ADR-019 (the layered limiter: an authenticated-only global cap that exempts this anonymous surface, plus the per-IP auth-ip window that now sits on the same two endpoints),…","i":"ICacheService Result Error"},{"u":"/docs/adr/030-startup-sole-migrator.html","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records"},{"u":"/docs/adr/030-startup-sole-migrator.html#status","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27)."},{"u":"/docs/adr/030-startup-sole-migrator.html#context","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Context","x":"Under database-per-service (ADR-006), each service owns its own database and its own migrations project, so something must apply pending migrations on every deploy. The…","i":"ApplicationSettings.DatabaseInitStrategy DatabaseInitializationExtensions EnsureCreated"},{"u":"/docs/adr/030-startup-sole-migrator.html#decision","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Decision","x":"In Azure Container Apps, every service host runs ApplicationSettingsDatabaseInitStrategy = Migrate in production and is the sole migrator of its own database: it applies its…","i":"ApplicationSettings__DatabaseInitStrategy __EFMigrationsHistory DatabaseInitStrategy MigrateAsync minReplicas deploy.yml migrations database Migrate dotnet sqlcmd update"},{"u":"/docs/adr/030-startup-sole-migrator.html#rationale","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Rationale","x":"- One migrator, one mechanism. The code that owns the schema applies the schema; there is no second tool to keep in lockstep and no ordering race between a deploy step and…","i":"__EFMigrationsHistory"},{"u":"/docs/adr/030-startup-sole-migrator.html#trade-offs","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Auto-migrate-in-production is what \"None\" exists to prevent. An unintended or destructive migration would ship itself on the next deploy. The apps accept this; the build-time…","i":"minReplicas"},{"u":"/docs/adr/030-startup-sole-migrator.html#related","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Related","x":"ADR-006 (database-per-service: why each service owns and migrates its own database), ADR-025 (readiness gating keeps traffic off a still-migrating replica), ADR-009 (RTO/RPO +…"},{"u":"/docs/adr/030-startup-sole-migrator.html#revision-2026-08-07","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"The sole-migrator decision extends to seed data: the same startup owner that applies the schema also runs the module seeders, in the same call, on the same boot. The Decision…","i":"moduleLoader.SeedAllAsync ModuleLoader.SeedAllAsync ConferenceModuleDbSeeder InitializeDatabaseAsync __EFMigrationsHistory DatabaseInitStrategy builder.Build IModuleSeeder ExistsAsync DbSeeder Guid int"},{"u":"/docs/adr/031-feature-flag-management.html","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records"},{"u":"/docs/adr/031-feature-flag-management.html#status","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27). Revised 2026-08-18 (a targeting-context accessor is now registered, so the built-in Targeting and Percentage filters give consistent per-user bucketing…"},{"u":"/docs/adr/031-feature-flag-management.html#context","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Context","x":"The apps need to decouple release from deploy: ship code dark, flip a kill switch, or roll a feature out to a percentage of users without a redeploy. A flag has to be enforceable…","i":"FeatureGate"},{"u":"/docs/adr/031-feature-flag-management.html#decision","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Decision","x":"Standardize on Microsoft.FeatureManagement, configured from the \"FeatureManagement\" configuration section and registered once in AddAPI (services.AddFeatureManagement() +…","i":"ApiControllerBase.HandleFailure Microsoft.FeatureManagement.Mvc IFeatureManager.IsEnabledAsync services.AddFeatureManagement FeatureGateCommandDecorator Microsoft.FeatureManagement FeatureGateQueryDecorator Error.NotFoundError ConferenceFeatures EngagementFeatures ErrorType.NotFound CatalogFeatures"},{"u":"/docs/adr/031-feature-flag-management.html#rationale","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Rationale","x":"- Release decoupled from deploy. A kill switch or a percentage rollout becomes a configuration change, not a code change: the central reason feature management exists. - Two…"},{"u":"/docs/adr/031-feature-flag-management.html#trade-offs","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Trade-offs","x":"- The two enforcement points must agree. A flag gated on the controller but not the handler (or vice versa) is a half-protected feature; no fitness rule asserts both are wired,…","i":"IsEnabledAsync"},{"u":"/docs/adr/031-feature-flag-management.html#revision-2026-08-18","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Progressive rollout is now usable, because the targeting context exists. The Decision above listed the Percentage / TimeWindow / Targeting filters as \"available\", and the last…","i":"CurrentUserTargetingContextAccessor FeatureGateCommandDecorator ITargetingContextAccessor featureGated.FeatureName AddHttpContextAccessor IHttpContextAccessor ICurrentUserService ClaimTypes.Role IFeatureManager IsEnabledAsync Identity.Name WithTargeting"},{"u":"/docs/adr/031-feature-flag-management.html#related","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Related","x":"ADR-014 (the decorator pipeline whose outermost slot FeatureGate fills, and the ordering that puts it first, now with Authorization registered directly inside it so a disabled…","i":"FeatureGate Groups Result Error"},{"u":"/docs/adr/032-password-hashing.html","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records"},{"u":"/docs/adr/032-password-hashing.html#status","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-29, adoption note revised 2026-07-06, registration note revised 2026-08-01)."},{"u":"/docs/adr/032-password-hashing.html#context","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Context","x":"Identity stores a credential as a (salt, hash) pair, never plaintext. The framework needs one canonical hasher that every consuming Identity flow shares, so the key-derivation…"},{"u":"/docs/adr/032-password-hashing.html#decision","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Decision","x":"Provide a single IPasswordHasher (MMCA.Common.Application.Interfaces.Infrastructure, IPasswordHasher.cs:6) with one implementation PasswordHasher…","i":"MMCA.Common.Application.Interfaces.Infrastructure CryptographicOperations.FixedTimeEquals RandomNumberGenerator.GetBytes AuthenticationServiceBase Rfc2898DeriveBytes.Pbkdf2 HashAlgorithmName.SHA512 IdentityModuleDbSeeder AuthenticationService ChangePasswordHandler LegacyHmacSaltSize AddInfrastructure ComputeLegacyHash"},{"u":"/docs/adr/032-password-hashing.html#rationale","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Rationale","x":"- One framework-owned primitive, not per-app crypto. Putting the algorithm, work factor, salt size, and comparison in a single shared type means a future hardening (raising…","i":"IsLegacy"},{"u":"/docs/adr/032-password-hashing.html#trade-offs","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Trade-offs","x":"- The legacy branch is a permanent correctness dependency that looks deletable. Its load-bearing role is invisible from the method body alone, so it is the single most…","i":"VerifyPassword Iterations"},{"u":"/docs/adr/032-password-hashing.html#related","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (cross-service JWT / JWKS authentication: the hasher gates credential verification that issues the tokens that ADR-004 then validates across services), ADR-005…","i":"EncryptedStringConverter"},{"u":"/docs/adr/033-resource-ownership-authorization.html","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records"},{"u":"/docs/adr/033-resource-ownership-authorization.html#status","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02, revised 2026-07-25)."},{"u":"/docs/adr/033-resource-ownership-authorization.html#context","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Context","x":"ADR-020 added a permission (capability) layer over RBAC: it answers \"what may this role do\", resolving a role to a permission so an endpoint can require a capability instead of a…","i":"OwnerOrAdminFilter GET"},{"u":"/docs/adr/033-resource-ownership-authorization.html#decision","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Decision","x":"Provide a row/resource-level ownership axis in MMCA.Common.API (the Authorization folder), with two enforcement points keyed on the caller's owner claim (customerid by default)…","i":"ShoppingCartsController.GetAllForLookupAsync ShoppingCartByCustomerSpecification ShoppingCartsController.GetAllAsync AggregateRootEntityControllerBase CustomersController.CreateAsync CustomersController.GetAllAsync OrdersByCustomerSpecification GetOwnershipSpecification OwnerOrAdminFilterOptions ICurrentUserService.Role OwnershipHelper.IsAdmin settings.OwnerClaimType"},{"u":"/docs/adr/033-resource-ownership-authorization.html#rationale","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Rationale","x":"- Reject-one and filter-many are genuinely two mechanisms. A single-resource route has an id to compare, so a short action filter that 403s on a mismatch is the cheapest correct…","i":"IEntityQueryService Specification Criteria TEntity And TId"},{"u":"/docs/adr/033-resource-ownership-authorization.html#trade-offs","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in per controller/handler. Neither point is automatic: a controller that forgets the [ServiceFilter] or omits the ownership spec from a query leaks across customers, the…","i":"OwnerOrAdminFilter ServiceFilter customer_id null"},{"u":"/docs/adr/033-resource-ownership-authorization.html#related","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Related","x":"ADR-020 (the role/permission RBAC layer this complements, and whose explicit 020-permission-based-authorization.md:78 scope-out this fills), ADR-034 (the generic entity query…","i":"IEntityQueryService Specification ForbidResult Result"},{"u":"/docs/adr/033-resource-ownership-authorization.html#revision-2026-07-25","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"An audit against the code. No behavior changed; the ADR text did. 1. The per-mutation check's failure shape was described as one branch, and it is two. ValidateOwnershipAsync was…","i":"ICurrentUserService.Role ValidateOwnershipAsync OwnerOrAdminFilter AllowMissingOwner OrdersController Error.Forbidden"},{"u":"/docs/adr/033-resource-ownership-authorization.html#revision-2026-08-01","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"Anchor-only correction. No behavior changed; OrdersController was refactored (a constructor parameter added, GetOwnershipSpecification() and the IsAdmin property extracted,…","i":"GetOwnershipSpecification ValidateOwnershipAsync OrdersController Error.Forbidden Error.NotFound IsAdmin"},{"u":"/docs/adr/034-generic-entity-query-layer.html","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/034-generic-entity-query-layer.html#status","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-30). Amended (2026-07-23): the filter strategy registry now also covers long/long? via LongFilterStrategy. Amended (2026-07-25): the keyed by-id fast path is…","i":"TryGetFastPathIncludes LongFilterStrategy long"},{"u":"/docs/adr/034-generic-entity-query-layer.html#context","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Context","x":"Every module exposes many entities, and most of them need the same read and write surface: list, page, look up for a dropdown, fetch by id, create, delete. Hand writing a…"},{"u":"/docs/adr/034-generic-entity-query-layer.html#decision","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Decision","x":"Give every entity a generic REST resource surface and a bounded dynamic query contract, supplied by two controller bases over a shared query pipeline. 1. Generic read controller.…","i":"EntityQueryPipeline.MaxUnboundedResultLimit QueryFieldService.ApplyFieldSelection QueryFilterService.RegisterStrategy IApplicationSettings.MaxPageSize QueryFilterService.ApplyFilters QueryFieldService.ApplySorting MaxUnboundedResultLimit QueryFilterModelBinder INavigationPopulator EntityQueryPipeline SupportedOperators IFilterStrategy"},{"u":"/docs/adr/034-generic-entity-query-layer.html#rationale","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Rationale","x":"- Write the resource surface once, inherit it everywhere. The verbs, routes, filter contract, pagination, and header are defined once on the two bases; a concrete controller…","i":"QueryFilterService.ValidateFilters MaxUnboundedResultLimit DTOToEntityPropertyMap INavigationPopulator DTOMapper.MapToDTOs SupportedOperators IEntityDTOMapper IFilterStrategy MaxPageSize"},{"u":"/docs/adr/034-generic-entity-query-layer.html#trade-offs","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- The wire contract tracks the entity model. Filterable, sortable, and projectable surface is the entity's property set. A model change is an API change unless mediated by the…","i":"QueryFilterService.ValidateFilters MaxUnboundedResultLimit DTOToEntityPropertyMap IFilterStrategy virtual"},{"u":"/docs/adr/034-generic-entity-query-layer.html#related","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-001 (manual DTO mapping: the generic controllers project through IEntityDTOMapper), ADR-002 (navigation populators: the unsupported-include path), ADR-013 (Result pattern at…","i":"IEntityDTOMapper HandleFailure result.Errors Idempotent"},{"u":"/docs/adr/034-generic-entity-query-layer.html#revision-2026-07-24","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Filter and pagination corrections from a code review. The contract shape is unchanged; these close cases where the engine widened a result set instead of narrowing it, or…","i":"IFilterStrategy.CanParseValue PaginationMetadata.PageSize MaxUnboundedResultLimit DTOToEntityPropertyMap Filter.Value.Invalid ValidateFilters FirstOrDefault TotalItemCount ApplyFilters GetByIdAsync int.MaxValue includeFKs"},{"u":"/docs/adr/034-generic-entity-query-layer.html#revision-2026-07-25","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"Citation maintenance from an ADR audit. No decision or behavior changed; the source anchors above were rebased to their current declaration and call-site lines. 1. The fast-path…","i":"IsPrimaryKeyOnlyLookup TryGetFastPathIncludes"},{"u":"/docs/adr/035-optimistic-concurrency.html","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records"},{"u":"/docs/adr/035-optimistic-concurrency.html#status","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02). Amended 2026-07-16: a child-entity overload of SetOriginalRowVersion was added (see Decision). Revised 2026-08-18: the same token gains an HTTP-native…","i":"SetOriginalRowVersion IConcurrencyAware SupportsIfMatch GetByIdAsync RowVersion ETag"},{"u":"/docs/adr/035-optimistic-concurrency.html#context","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Context","x":"Every mutable aggregate in the framework is edited through a load-modify-save handler: the update use case fetches the tracked entity, applies the request, and calls…","i":"SaveChangesAsync Conflict"},{"u":"/docs/adr/035-optimistic-concurrency.html#decision","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Decision","x":"Give every auditable entity a database-managed RowVersion concurrency token, round-trip it through the client on updates, and stamp the client's last-seen value as EF's original…","i":"MMCA.Common.Domain.Interfaces.IRowVersioned IWriteRepository.SetOriginalRowVersion ConcurrencyConventionTestsBase MMCA.Store.Architecture.Tests DbUpdateConcurrencyException MMCA.ADC.Architecture.Tests AddRowVersionToAllEntities ConfigureConcurrencyTokens DbUpdateExceptionHandler SetOriginalRowVersion AuditableBaseEntity ErrorType.Conflict"},{"u":"/docs/adr/035-optimistic-concurrency.html#rationale","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Rationale","x":"- Database-managed token over a hand-maintained version field. A SQL Server rowversion auto-increments on the server on every write; no domain code sets or reads it (the setter…","i":"DbUpdateExceptionHandler SetOriginalRowVersion DbUpdateException rowversion WHERE"},{"u":"/docs/adr/035-optimistic-concurrency.html#trade-offs","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in at the caller, not just the type. A null or empty RowVersion skips the check, so a client that never echoes the token still gets last-write-wins. The fitness function…","i":"AddRowVersionToAllEntities DbUpdateExceptionHandler IsConcurrencyToken DbUpdateException UpdateRequest rowversion RowVersion byte"},{"u":"/docs/adr/035-optimistic-concurrency.html#revision-2026-08-18","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"This record chose a body round-trip: the client echoes RowVersion on the update request. HTTP has had a standard way to say the same thing since long before this framework, ETag…","i":"RewriteConflictToPreconditionFailed EntityControllerBase.GetByIdAsync UpdateRequestsAreConcurrencyAware DbUpdateConcurrencyException DbUpdateExceptionHandler SupportsIfMatchAttribute ServiceFilterAttribute SetOriginalRowVersion IAsyncActionFilter SetConcurrencyETag HttpContext.Items IConcurrencyAware"},{"u":"/docs/adr/035-optimistic-concurrency.html#related","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Related","x":"ADR-017 (HTTP request idempotency, which dedups retries of the same request, the mirror-image concern to two distinct edits racing here, and whose own 2026-08-18 revision adds…","i":"AuditableBaseEntity GetByIdAsync RowVersion"},{"u":"/docs/adr/036-external-oauth-login.html","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records"},{"u":"/docs/adr/036-external-oauth-login.html#status","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02, migration attribution corrected 2026-07-06, native-callback redirect branch added 2026-07-17 per ADR-043, email-verified account-takeover guard before…"},{"u":"/docs/adr/036-external-oauth-login.html#context","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Context","x":"The framework's Identity story so far is entirely first-party: a user registers with an email and password, the credentials are hashed (ADR-032), and Identity mints its own RS256…","i":"AddPermissions User"},{"u":"/docs/adr/036-external-oauth-login.html#decision","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Decision","x":"Add an opt-in external-login path that federates Google/GitHub sign-in at the edge and immediately exchanges the external identity for the app's own local JWT pair, linking the…","i":"IAuthenticationService.ExternalLoginAsync OAuthControllerBase.CompleteAsync AddExternalLoginProviderFields Auth.ExternalLoginNotSupported Auth.ExternalEmailNotVerified ConfigurationOAuthUISettings Auth.ExternalEmailInvalid User.LinkExternalProvider AddExternalAuthProviders AddCommonAuthentication AuthenticationResponse IAuthenticationService"},{"u":"/docs/adr/036-external-oauth-login.html#rationale","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Rationale","x":"- Terminate federation at the edge, keep one internal identity. Exchanging the external principal for a local JWT the moment the callback returns means every downstream concern…","i":"ExternalLoginAsync ClientId POST User GET"},{"u":"/docs/adr/036-external-oauth-login.html#trade-offs","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in per app, and easy to half-wire. The flow needs four cooperating pieces (scheme registration, the controller subclass, the service override, and the OAuthUIBaseUrl…","i":"Auth.ExternalLoginNotSupported Auth.ExternalEmailNotVerified IExternalLoginEmailVerifier OAuth__UIBaseUrl IsExternalLogin email_verified ExternalLogin LoginProvider ProviderKey ClientId User"},{"u":"/docs/adr/036-external-oauth-login.html#related","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the RS256/JWKS token this flow exchanges the external identity for, and validates everywhere after), ADR-022 (the browser cookies that carry the resulting session),…","i":"User.Anonymize CompleteAsync"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#status","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-06; revised 2026-07-24, 2026-07-25, 2026-08-15, 2026-08-18). Revised 2026-08-18: the versioned-envelope converter is no longer unpublished, it is included in…"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#context","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Context","x":"Transparent database encryption (TDE) protects the data files as a whole, but it decrypts transparently for anyone who can query the database, so a leaked backup restored on a…"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#decision","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Decision","x":"Provide a single framework-owned EF Core value converter that transparently encrypts string columns at rest with authenticated encryption, applied per property in an entity…","i":"MMCA.Common.Infrastructure.Persistence.Encryption ArgumentNullException.ThrowIfNull RandomNumberGenerator.GetBytes EncryptedStringConverterTests MMCA.Common.Infrastructure EncryptedStringConverter CryptographicException IReadOnlyDictionary ArgumentException FromBase64String FrozenDictionary AesGcm.Decrypt"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#rationale","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Rationale","x":"- Authenticated, not merely confidential. AES-GCM binds a 128-bit tag to the ciphertext (EncryptedStringConverter.cs:81, :201), so a tampered or truncated value fails to decrypt…","i":"AesGcm.Decrypt string"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#trade-offs","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Latent today, proven by tests rather than production. The plumbing is complete and unit-tested, but no entity configuration wires it, so the encrypt/decrypt round-trip, the…","i":"EncryptedStringConverterTests CryptographicException HasConversion byte"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#related","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (soft-delete vs erasure: the other sensitive-data control, which names this converter as the mechanism for erasure fields that must stay retrievable,…"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#revision-2026-07-24","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Documented a constraint the converter always had but did not state: the ciphertext is non-deterministic. Every write uses a fresh random nonce, which is the correct property for…","i":"Email Where"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#revision-2026-07-25","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"Documentation-only correction, no behavior change. Item 1 of the Decision still illustrated the converter with builder.Property(e = e.Email), contradicting the 2026-07-24…","i":"EncryptedStringConverter.cs SocialSecurityNumber builder.Property e.Email"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#revision-2026-08-15","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Revision (2026-08-15)","x":"Behavior change, not a documentation correction. The stored layout is now a versioned envelope: Base64 of [key version (1)] [nonce (12)] [ciphertext (N)] [tag (16)] rather than…","i":"SaveChanges ciphertext DbContext version nonce main key tag"},{"u":"/docs/adr/038-supply-chain-provenance.html","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records"},{"u":"/docs/adr/038-supply-chain-provenance.html#status","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-06; revised 2026-07-21)."},{"u":"/docs/adr/038-supply-chain-provenance.html#context","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common is a published framework: it packs its NuGet packages and pushes them to GitHub Packages on every v tag (release.yml:3-5), where the two production apps and the…","i":"Directory.Build.props nuget.config"},{"u":"/docs/adr/038-supply-chain-provenance.html#decision","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Decision","x":"Treat supply-chain integrity as a set of build-gating controls, the same invariant-over-discipline posture ADR-015 applies to architecture rules. Four controls, each a hard gate:…","i":"SQLitePCLRaw.bundle_e_sqlite3 RestorePackagesWithLockFile MMCA.Common.Infrastructure Directory.Packages.props Directory.Build.props TreatWarningsAsErrors packageSourceMapping NuGetAuditSuppress packages.lock.json MMCA.Common.slnx nuget.config NuGetAudit"},{"u":"/docs/adr/038-supply-chain-provenance.html#rationale","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Rationale","x":"- Provenance is a gate, not a document. A hard-failing SBOM step means the bill of materials cannot silently go missing on a release: the artifact is produced or the release…","i":"Directory.Build.props NuGetAuditSuppress dotnet list"},{"u":"/docs/adr/038-supply-chain-provenance.html#trade-offs","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The SBOM is generated and archived, not yet signed or attested. The gate proves a bill of materials exists for each release (release.yml:58); it does not add cryptographic…","i":"NuGetAuditSuppress nuget.config dotnet list"},{"u":"/docs/adr/038-supply-chain-provenance.html#related","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Related","x":"ADR-016 (lockstep versioning + the MassTransit-v8 license pin; this record extends dependency governance from versioning and licensing into supply-chain provenance and…"},{"u":"/docs/adr/039-live-channel-push.html","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records"},{"u":"/docs/adr/039-live-channel-push.html#status","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-09)."},{"u":"/docs/adr/039-live-channel-push.html#context","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Context","x":"Conference-day features (live polls, session Q&A, live result counters) need sub-second fan-out of small events to whoever is looking at a page right now. The existing…"},{"u":"/docs/adr/039-live-channel-push.html#decision","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Decision","x":"One realtime transport, two publisher boundaries: - NotificationHub stays the single hub and gains its first client-invokable methods: JoinChannel / LeaveChannel map the calling…","i":"PushNotificationSettings.ChannelKeyPattern SignalRLiveChannelPublisher NullLiveChannelPublisher IPushNotificationSender NotificationHubService ILiveChannelPublisher AddPushNotifications ReceiveChannelEvent LeaveChannelAsync JoinChannelAsync NotificationHub OnChannelEvent"},{"u":"/docs/adr/039-live-channel-push.html#rationale","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Rationale","x":"- One WebSocket per client keeps connection management, token refresh, reconnect, and backplane behavior in one place; channel membership is a property of the existing…","i":"IMessageBus"},{"u":"/docs/adr/039-live-channel-push.html#trade-offs","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Ephemeral means lossy: a client that connects after an event was published never sees it. Features must treat channel events as cache-invalidation hints over fetchable state,…","i":"NotificationCallback"},{"u":"/docs/adr/039-live-channel-push.html#revision-2026-07-24","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Two corrections from a code review; the best-effort, per-session-ordered decision is unchanged. 1. Broadcasts are enqueued after commit, not during the command. CastVoteHandler…","i":"BoundedChannelFullMode.DropOldest SessionQuestionUpvoteChanged LivePollVoteChanged ToggleUpvoteHandler CastVoteHandler DroppedCount itemDropped TryWrite"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#status","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-10). Amended (2026-07-10): explicit query-string variance parity with the built-in default policy (the initial release accidentally dropped it, collapsing every…","i":"OutputCacheEvictionRequested ContentEditor SponsorsCache NowNextCache bypassRoles Organizer"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#context","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Context","x":"The framework's read-scaling design leans on ASP.NET Core output caching: anonymous-readable endpoints ([AllowAnonymous] GETs like event/session/speaker catalogs) carry named…","i":"AuthDelegatingHandler BookmarkCountsCache AllowAnonymous Authorization NowNextCache"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#decision","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Decision","x":"MMCA.Common.API ships PublicEndpointOutputCachePolicy, an IOutputCachePolicy that mirrors the built-in default policy with one deliberate difference: it does not disable cache…","i":"ConferenceReadAudience.PrivilegedRoles PublicEndpointOutputCachePolicy DbUpdateConcurrencyException IOutputCachePolicy MMCA.Common.API AllowAnonymous Authorization ContentEditor NowNextCache extension Organizer reference"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#rationale","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Rationale","x":"- The response payload, not the request's auth state, is what determines cacheability. For a user-independent payload, Authorization is noise; refusing to cache on it turns the…","i":"Authorization"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#trade-offs","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Trade-offs","x":"- Consumers must audit which named policies move to AddPublicEndpointPolicy. Policies on permission-gated endpoints (e.g. an organizer dashboard) must NOT move; if such an…","i":"UserSessionBookmarkCacheEvictionHandler RegisterOutputCacheEvictionConsumer AddStackExchangeRedisOutputCache AddOutputCacheEvictionHandler OutputCacheEvictionRequested AddRedisDistributedCache AddPublicEndpointPolicy BookmarkCountsCache IDistributedCache EvictByTagAsync AddOutputCache NowNextCache"},{"u":"/docs/adr/041-observability-and-telemetry.html","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records"},{"u":"/docs/adr/041-observability-and-telemetry.html#status","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-10). Amended (2026-07-23) to document the Telemetry:DisableHttpClientMetrics and Telemetry:DisableRuntimeMetrics cost knobs and to correct the…","i":"MMCA.Common.OutputCache MMCA.Common.BestEffort OutboxProcessor RecordDuration OutboxMetrics OutboxProcess HttpClient finally reason"},{"u":"/docs/adr/041-observability-and-telemetry.html#context","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Context","x":"The framework is a modular monolith whose modules extract into standalone services (ADR-008), so the same telemetry has to make sense whether a request stays in one process or…","i":"HttpClient"},{"u":"/docs/adr/041-observability-and-telemetry.html#decision","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Decision","x":"Standardize telemetry in the shared Aspire service defaults, add framework-specific instrumentation for the CQRS and outbox paths, and expose cost knobs with fail-safe defaults.…","i":"APPLICATIONINSIGHTS_CONNECTION_STRING CqrsMetrics.CommandDuration.Record HttpContext.TraceIdentifier OTEL_EXPORTER_OTLP_ENDPOINT AddOpenTelemetryExporters IsInstrumentationDisabled OutboxPollFilterProcessor outbox.dead_letter.count TraceIdRatioBasedSampler CorrelationIdMiddleware ConfigureOpenTelemetry TryGetTraceSampleRatio"},{"u":"/docs/adr/041-observability-and-telemetry.html#rationale","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Rationale","x":"- Instrument only where auto-instrumentation is blind. The CQRS RED histograms and the outbox dead-letter counter cover the two framework-owned hot paths; everything else (HTTP,…","i":"ParentBased HttpClient true"},{"u":"/docs/adr/041-observability-and-telemetry.html#trade-offs","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Trade-offs","x":"- Custom instrumentation carries a maintenance cost. The Aspire package has no reference to Application or Infrastructure by design, so the meter and activity-source names are…","i":"OutboxProcess ParentBased"},{"u":"/docs/adr/041-observability-and-telemetry.html#revision-2026-08-18","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Two meters and one hop. Two new failure counters, each on its own meter. cache.eviction.failed, tagged cachetag, on MMCA.Common.OutputCache…","i":"GatewayCorrelationMiddleware besteffort.dispatch.failed CorrelationIdMiddleware MMCA.Common.Idempotency MMCA.Common.OutputCache MMCA.Common.BestEffort cache.eviction.failed MMCA.Common.Scheduler MMCA.Common.Broker MMCA.Common.Outbox MMCA.Common.Cqrs Idempotency"},{"u":"/docs/adr/041-observability-and-telemetry.html#related","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox whose dead-letter counter and poll-span filtering this defines), ADR-014 (the CQRS decorator pipeline that emits the RED histograms as a byproduct of its…","i":"AddServiceDefaults"},{"u":"/docs/adr/042-device-capability-abstraction.html","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records"},{"u":"/docs/adr/042-device-capability-abstraction.html#status","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-10, amended 2026-07-17, 2026-07-23 and 2026-08-14)."},{"u":"/docs/adr/042-device-capability-abstraction.html#context","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Context","x":"The consumer apps ship the same Blazor component set through three heads: MAUI Blazor Hybrid (Android/iOS/MacCatalyst/Windows), Blazor Server SSR, and WebAssembly. Native device…","i":"builder.Services.AddCommonMauiTokenStorage ITokenStorageService navigator.clipboard navigator.onLine navigator.share MMCA.Common.UI AddUIShared"},{"u":"/docs/adr/042-device-capability-abstraction.html#decision","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Decision","x":"Add a per-capability contract layer to MMCA.Common.UI and a fifteenth package, MMCA.Common.UI.Maui, carrying the native implementations. - One small interface per capability, no…","i":"IExternalLinkService.InterceptsLinks AddBrowserDeviceCapabilities AddDeviceCapabilityDefaults EnforceUIMauiLayerBoundary IConnectivityStatusService AddMauiDeviceCapabilities ILocalNotificationService UseMauiDeviceCapabilities Directory.Packages.props IPushDeviceTokenProvider IPushRegistrationService MauiBackNavigationBridge"},{"u":"/docs/adr/042-device-capability-abstraction.html#rationale","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Rationale","x":"- A god IDeviceCapabilities interface would force every head to implement everything and turn each new capability into a breaking change; per-capability contracts are open/closed…","i":"IDeviceCapabilities AddUIShared"},{"u":"/docs/adr/042-device-capability-abstraction.html#trade-offs","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A fifteenth package raises release surface: two runners must both succeed for a whole release. Accepted; the publish-maui job is gated by the same tag and SBOM discipline. -…","i":"AddMauiDeviceCapabilities UseMauiDeviceCapabilities MauiExternalAuthBroker AddUIShared IsAvailable IsSupported false"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#status","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-07-28 (the Android https App Links leg is recorded as shipped, the outstanding Android item is restated as the served certificate fingerprint,…","i":"REPLACE_WITH_PLAY_APP_SIGNING_SHA256_FINGERPRINT WebAuthenticatorCallbackActivity MapAppAssociationEndpoints sha256_cert_fingerprints MauiExternalAuthBroker AppAssociationOptions IDeepLinkDispatcher assetlinks.json MMCA.ADC.UI.Web CompleteAsync MainActivity AutoVerify"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#context","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Context","x":"Three mobile flows all need a URL to leave the web world and land inside the MAUI app: 1. Shared links and QR codes. The share sheet and QR codes carry ordinary https web URLs.…","i":"OAuthControllerBase.CompleteAsync IDeepLinkDispatcher WebAuthenticator assetlinks.json CompleteAsync"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#decision","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Decision","x":"- Custom-scheme returnUrl allowlist in the framework. CompleteAsync consults OAuth:AllowedReturnUrlSchemes (a config array, default empty). When the challenge's stashed returnUrl…","i":"IAuthUIService.ExchangeOAuthCodeAsync WebAuthenticatorCallbackActivity ITokenStorageService IDeepLinkDispatcher IExternalAuthBroker Uri.OriginalString CFBundleURLTypes WebAuthenticator assetlinks.json CompleteAsync AutoVerify returnUrl"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#rationale","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Rationale","x":"- Reusing the single-use-code exchange keeps the token-never-in-URL invariant identical across web and native; the only new surface is WHERE the code lands. - A scheme allowlist…"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#trade-offs","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Trade-offs","x":"- The app-facing hostname is baked into store binaries (intent filters, entitlements). The apps currently ride the Azure Container Apps default domain, which changes if the…","i":"appsettings.json EmbeddedResource PublicWebHost"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#revision-2026-07-28","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Revision (2026-07-28)","x":"Correction pass from an ADR audit. No decision or behavior changed; the Status section had the Android leg backwards and the Decision section attributed the token exchange to the…","i":"IAuthUIService.ExchangeOAuthCodeAsync ITokenStorageService.SetTokensAsync MapAppAssociationEndpoints sha256_cert_fingerprints AndroidCertFingerprints BuildSuccessRedirectUrl IDeepLinkDispatcher WebAuthenticator CompleteAsync IntentFilter MainActivity OnNewIntent"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#revision-2026-08-01","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"Status pass from an ADR audit. No decision and no behavior changed; the one item the previous revision left open is closed, and the anchor that revision itself introduced had…","i":"MapAppAssociationEndpoints sha256_cert_fingerprints AndroidCertFingerprints AndroidPackageName assetlinks.json ApplicationId Program.cs d5fd0e9"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#revision-2026-08-07","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Anchor and precision pass from an ADR audit. No decision and no behavior changed. 1. The two Program.cs anchors moved one line. MMCA.ADC commit 886fa189 (PR 100, merged…","i":"app.MapAppAssociationEndpoints AndroidCertFingerprints AppAssociationOptions AndroidPackageName PublicWebHost GetSection Program.cs new"},{"u":"/docs/adr/044-native-push-delivery.html","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records"},{"u":"/docs/adr/044-native-push-delivery.html#status","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-11). Amends ADR-024. The framework pipeline is implemented and inert by default; each consumer switches it on by provisioning a notification hub with platform…","i":"NativePush"},{"u":"/docs/adr/044-native-push-delivery.html#context","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Context","x":"ADR-024 established two notification channels: a durable per-user UserNotification inbox (the source of truth) and a transient SignalR push behind IPushNotificationSender. Both…","i":"IPushNotificationSender UserNotification"},{"u":"/docs/adr/044-native-push-delivery.html#decision","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Decision","x":"- Azure Notification Hubs as the delivery fan-out. One hub abstracts both platforms behind one API, holds the platform credentials outside our code, and its installation model…","i":"INativePushSender.SendToUsersAsync Notification.PushNotifications MauiPushRegistrationService NullPushDeviceTokenProvider SendPushNotificationHandler AddNativePushNotifications AddNotificationControllers AuthUIService.LogoutAsync IPushDeviceTokenProvider IPushRegistrationService PushRegistrationListener IPushDeviceRegistrar"},{"u":"/docs/adr/044-native-push-delivery.html#consequences","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Consequences","x":"- Sends fan out per 20-user chunk and per platform: an audience of N users costs ceil(N/20) 2 hub calls. Acceptable at conference scale; a template-based send can consolidate…","i":"SendPushNotificationHandler ceil"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#status","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-11). Records the BR-116 amendment (ADC): avatar photos are IN scope, powered by two new framework extension points. The framework legs are implemented; each…"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#context","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Context","x":"The MAUI capability program (ADR-042) brought MediaPicker/camera within reach, and ADC amended BR-116 to include user avatar photos. That needs binary blob storage (the databases…"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#decision","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Decision","x":"- IFileStorageService (Application): upload-by-blob-name returning the public URI, plus idempotent delete. Default is an unconfigured Null implementation whose uploads fail with…","i":"ImageSharpImageProcessor AddAzureBlobFileStorage IFileStorageService IMediaPickerService ConnectionString IImageProcessor configuration ContainerName FileStorage IsSupported ServiceUri InputFile"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#consequences","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Consequences","x":"- The avatars container is public-read by design: avatar URLs render in tags on anonymous-visible surfaces without SAS plumbing. The random blob suffix prevents enumeration; the…","i":"DefaultAzureCredential img"},{"u":"/docs/adr/046-http-api-versioning.html","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records"},{"u":"/docs/adr/046-http-api-versioning.html#status","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-08-01 (anonymity is granted by each per-service subclass, not by ServiceInfoControllerBase; corrected the ADR-034 cross-reference, which puts…","i":"ServiceInfoControllerBase AddCommonApiVersioning EntityControllerBase DefaultApiVersion Asp.Versioning"},{"u":"/docs/adr/046-http-api-versioning.html#context","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Context","x":"The framework's REST surface is served by controllers hosted in extracted service processes behind a YARP gateway. As those services evolve, a response shape has to be able to…","i":"Asp.Versioning SchemaVersion v1.0"},{"u":"/docs/adr/046-http-api-versioning.html#decision","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Decision","x":"Standardize one header-based API-versioning setup in MMCA.Common.API, adopt it in every service host through a single registration call, and keep it exercised by a shared fitness…","i":"ApiParameterDescription.ParameterDescriptor ApiParameterDescriptorBackfillProvider ServiceInfoVersioningContractTestsBase AssumeDefaultVersionWhenUnspecified ServiceInfoControllerBase SubstituteApiVersionInUrl IApiDescriptionProvider AddCommonApiVersioning Asp.Versioning.OpenApi HeaderApiVersionReader ServiceInfoController ServiceInfoV2Response"},{"u":"/docs/adr/046-http-api-versioning.html#rationale","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Rationale","x":"- Header selection keeps URLs stable. Routing stays version-free, so gateway route maps, client URL builders, and OpenAPI paths do not fork per version; a caller opts into a…","i":"AssumeDefaultVersionWhenUnspecified ServiceInfoControllerBase AddCommonApiVersioning ReportApiVersions ServiceInfo"},{"u":"/docs/adr/046-http-api-versioning.html#trade-offs","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Trade-offs","x":"- The class-level version attributes are not inherited. Each per-service subclass must repeat the [ApiVersion(...)] and routing attributes (the same inheritance caveat ADR-036…","i":"AddCommonApiVersioning MapCommonOpenApi OAuthController ApiVersion"},{"u":"/docs/adr/046-http-api-versioning.html#related","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Related","x":"ADR-010 (integration-event schema versioning: the asynchronous, SchemaVersion-carried, consumer-resolved axis this deliberately contrasts with; HTTP versioning here is…","i":"ServiceInfoVersioningContractTestsBase OAuthController ApiController SchemaVersion ApiVersion controller Route"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#status","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15)."},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#context","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Context","x":"Soft-delete is the framework's default deletion model (ADR-005): AuditableBaseEntity.Delete() sets IsDeleted = true and EF global query filters hide the row, but the record…","i":"AuditableBaseEntity.Delete HttpContext.User IsDeleted true"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#decision","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Decision","x":"Add a shared-pipeline middleware, SoftDeletedUserMiddleware (Source/Presentation/MMCA.Common.API/Middleware/SoftDeletedUserMiddleware.cs:31, BR-133), that rejects an…","i":"DeleteUserHandler.OnAfterSoftDeleteAsync SoftDeletedUserCache.MarkDeletedAsync SoftDeletedUserCache.MarkerDuration context.RequestServices.GetService SoftDeletedUserMiddlewareTests AuditableAggregateRootEntity SoftDeletedUserCache.KeyFor UseCommonMiddlewarePipeline ICurrentUserService.UserId ISoftDeletedUserValidator SoftDeletedUserMiddleware SoftDeletedUserValidator"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#rationale","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Rationale","x":"- Bounds the stateless-JWT revocation gap cheaply. Stateless JWT (ADR-004) has no built-in revocation, so a deactivated account would otherwise stay usable for the full remaining…","i":"ISoftDeletedUserValidator SoftDeletedUserValidator MMCA.Common.API TUser User"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#trade-offs","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Trade-offs","x":"- Revocation is bounded, not immediate. A soft-deleted user whose status is cached as not-deleted keeps passing until that cache entry expires (up to 30 seconds), unless the…","i":"SoftDeletedUserCache.MarkDeletedAsync SoftDeletedUserCache.MarkerDuration ISoftDeletedUserValidator DeleteUserHandler"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#related","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (soft-delete is the deletion model whose still-authenticated tokens this middleware revokes; deleting a user is a soft-delete, not a row removal), ADR-004 (the stateless…"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#revision-2026-08-07","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Re-verified against current source. The decision is unchanged, but three things it described have moved: the validator implementation, the home of the 30-second constant, and the…","i":"SoftDeletedUserCache.MarkerDuration SoftDeletedUserMiddleware SoftDeletedUserValidator TimeSpan.FromSeconds DeleteUserHandler CacheDuration UserId TUser true User"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#status","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-07-21 (corrected the empty-placeholder-folder inventory and the Directory.Build.props and ADC User source citations). Revised 2026-07-28…","i":"Directory.Build.props SponsorIdentifierType UserIdentifierType StronglyTypedIds User"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#context","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Context","x":"Every entity needs an identity type. The framework's base entity is generic over that type: BaseEntity constrains it to notnull and exposes a single required init Id…","i":"UserIdentifierType TIdentifierType IBaseEntity BaseEntity readonly required notnull record struct UserId Value Guid"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#decision","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Decision","x":"Model every identifier as a primitive named through a global-using alias, declared per module, not as a wrapper struct. - Identity is a primitive behind an alias. Each module…","i":"EntityTypeConfigurationSQLServer AuditableAggregateRootEntity AuthenticationServiceBase Directory.Build.props SpeakerIdentifierType AuditableBaseEntity UserIdentifierType LinkedSpeakerId IdentifierType LastModifiedBy GetRepository System.Guid"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#rationale","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Rationale","x":"- Readable signatures at zero runtime cost. GetRepository () reads as intent while the CLR sees a plain int. There is no allocation, boxing, or wrapper indirection per…","i":"UserIdentifierType IEntityDTOMapper System.Text.Json GetRepository JsonConverter IBaseEntity BaseEntity IBaseDTO Shared Guid User int"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#trade-offs","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Trade-offs","x":"- No compile-time protection against swapping same-typed identifiers. An alias is a type synonym, not a distinct type. Because most aliases resolve to int, the compiler will not…","i":"SessionIdentifierType SpeakerIdentifierType UserIdentifierType Shared Guid int"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#related","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Related","x":"ADR-001 (the per-entity DTO mappers are parameterized by this identifier type, IEntityDTOMapper ), ADR-034 (the generic entity controllers and query contract ride on the same…","i":"IEntityDTOMapper TIdentifierType TEntityDTO TEntity Shared"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#revision-2026-08-18","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"No decision, no behavior and no citation in this record changed. What changed is the standing of the deferral it records. The last Trade-offs entry above (\"Revisiting the trade…","i":"UserIdentifierType CheckIn Source int"},{"u":"/docs/adr/049-library-configureawait-policy.html","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records"},{"u":"/docs/adr/049-library-configureawait-policy.html#status","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-20; measurements re-anchored 2026-08-07, 2026-08-14 and 2026-08-18)."},{"u":"/docs/adr/049-library-configureawait-policy.html#context","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common ships as NuGet packages consumed by host applications, not as an application itself. Library code that awaits without ConfigureAwait(false) captures the caller's…","i":"SynchronizationContext MMCA.Common.UI.Maui ConfigureAwait editorconfig VSTHRD111 RCS1090 CA2007 MA0004 false"},{"u":"/docs/adr/049-library-configureawait-policy.html#decision","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Decision","x":"Packaged non-UI framework code awaits with ConfigureAwait(false); UI component packages and application code do not. - Enforcement is a build gate, not a convention. The…","i":"TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI editorconfig VSTHRD111 RCS1090 warning CA2007 MA0004 false"},{"u":"/docs/adr/049-library-configureawait-policy.html#rationale","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Rationale","x":"- Correctness for the one consumer that already has a context. The MAUI head consumes Infrastructure/Application/API packages through DI; a sync-over-async call anywhere in that…","i":"ConfigureAwait GetAwaiter GetResult script batch false fixes place step but"},{"u":"/docs/adr/049-library-configureawait-policy.html#trade-offs","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Visual noise in framework source. Every await in Source/ (except UI packages) carries .ConfigureAwait(false) (324 sites at adoption; 718 gated sites as of the 2026-08-18…","i":"ConfigureAwait editorconfig false"},{"u":"/docs/adr/049-library-configureawait-policy.html#related","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Related","x":"ADR-042 (the MAUI package whose synchronization context motivates the policy), ADR-027 (the same \"machine-boundary hygiene as a build gate\" posture applied to culture-explicit…"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-07","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"An audit against the code. The policy did not change; three statements about it did. 1. The exemption covers three packages, not the two the Decision named. The glob is…","i":"CodeAnalysisTreatWarningsAsErrors ServerTokenStorageService TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers PackageId foreach warning CA2007"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-14","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-14)","x":"A re-measurement only. The policy, the gate and the exemption are unchanged; the counts the document quotes were a week old and had moved by roughly 9%. 1. Framework site counts,…","i":"CodeAnalysisTreatWarningsAsErrors ServerTokenStorageService TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers PackageId warning CA2007 dotnet"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-18","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"A re-measurement only, in the same terms as the 2026-08-14 pass. The policy, the gate and the exemption are unchanged; two of the three counted figures moved. 1. Framework site…","i":"CodeAnalysisTreatWarningsAsErrors TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers warning CA2007 dotnet format await"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#status","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-21)."},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#context","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Context","x":"Identity issues two credentials on every successful sign-in: a short-lived, stateless JWT access token that every service validates by signature and expiry (ADR-004), and a…","i":"AuthenticationServiceBase TUser"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#decision","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Decision","x":"Mint a stateless JWT access token plus a single, server-stored refresh token that rotates on every use, with a token mismatch triggering revocation. - Access token is stateless;…","i":"TokenService.GetPrincipalFromExpiredToken JwtSettings.AccessTokenExpirationMinutes JwtSettings.RefreshTokenExpirationDays TokenService.GenerateRefreshToken TokenService.RefreshTokenLifetime TokenService.GenerateAccessToken RandomNumberGenerator.GetBytes user.RevokeRefreshToken user.UpdateRefreshToken AuthenticationService RefreshTokenLifetime RefreshTokenExpiry"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#rationale","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Rationale","x":"- Short access token plus refresh keeps the hot path stateless. Every service validates the access token with no store lookup (ADR-004); the short exp bounds the revocation gap,…","i":"exp"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#trade-offs","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Trade-offs","x":"- One refresh token per user means one live session. A new login overwrites the single stored token (AuthenticationServiceBase.cs:298), so signing in on a second device…","i":"JwtSettings.RefreshTokenExpirationDays RefreshTokenExpirationDays RefreshTokenLifetime TimeSpan.Zero TokenService"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#related","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the stateless RS256/JWKS access token this refresh flow reissues, and the algorithm pinning GetPrincipalFromExpiredToken relies on), ADR-032 (the password hashing that…","i":"GetPrincipalFromExpiredToken AuthenticationServiceBase TUser"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#status","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-23). Revised 2026-08-14 (SetTokensAsync now writes the refresh token and the access token under one shared guard, so a failed refresh-token write also drops…","i":"SetTokensAsync"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#context","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Context","x":"ADR-022 and ADR-050 describe the two server halves of authentication: the Blazor host's HttpOnly session cookie that survives SSR prerender (ADR-022), and the Identity service's…","i":"HttpContext"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#decision","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Decision","x":"Model the client token lifecycle as two small abstractions (ITokenStorageService for persistence, ITokenRefresher for reacquisition) plus a shared bearer-attaching handler and a…","i":"AddClientAuthSessionCookieSync JwtAuthenticationStateProvider SameOriginProxyTokenRefresher ISessionCookieSync.SyncAsync AddCommonServerTokenStorage AddCommonMauiTokenStorage ServerTokenStorageService mmcaAuthSession.getToken NotifyUserAuthentication AcquireAccessTokenAsync DirectApiTokenRefresher WasmTokenStorageService"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#rationale","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Rationale","x":"- One application surface, three storage stories. Pages, services, and the HTTP pipeline talk to ITokenStorageService and AuthenticationStateProvider only; the head-specific…","i":"AuthenticationStateProvider ITokenStorageService"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#trade-offs","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Trade-offs","x":"- The browser heads depend on the same-origin UI host. SameOriginProxyTokenRefresher only works where the UI host serves the /auth/session/ endpoints; a browser head deployed…","i":"JwtAuthenticationStateProvider SameOriginProxyTokenRefresher MMCA.Common.UI.Maui MMCA.Common.UI.Web MMCA.Common.slnx MMCA.Common.UI AuthorizeView SecureStorage"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#related","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Related","x":"ADR-022 (the Blazor host's HttpOnly session cookie and the /auth/session/ endpoints the browser refresher proxies through), ADR-050 (the single rotating refresh token with reuse…","i":"DirectApiTokenRefresher MauiTokenStorageService"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#revision-2026-08-07","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"The MAUI half of ITokenStorageService is no longer app-local. The original Decision left the SecureStorage-backed implementation in each app because it depends on the MAUI…","i":"JwtAuthenticationStateProvider MauiTokenStorageService.cs AddCommonMauiTokenStorage DirectApiTokenRefresher MauiTokenStorageService SecureStorage.Default ITokenStorageService MMCA.Common.UI.Maui auth_refresh_token auth_access_token ClearTokensAsync MMCA.Common.slnx"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#revision-2026-08-14","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Revision (2026-08-14)","x":"SetTokensAsync closed a gap the original hoist left open. Point 3 above previously described the method as writing the refresh token first and dropping both tokens only when the…","i":"SetTokensAsync catch try"},{"u":"/docs/adr/052-background-job-execution.html","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records"},{"u":"/docs/adr/052-background-job-execution.html#status","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-24)."},{"u":"/docs/adr/052-background-job-execution.html#context","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Context","x":"Some requests trigger work that cannot run inside the request: an AI scoring pass over an event's sessions takes minutes and issues one paid API call per session, and a…","i":"RunScoringInBackgroundAsync IHostApplicationLifetime eventId"},{"u":"/docs/adr/052-background-job-execution.html#decision","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Decision","x":"In-process background work runs as a bounded queue plus a single-reader hosted drain. Nothing starts an untracked Task from a request. - A bounded Channel per job kind,…","i":"BoundedChannelFullMode.DropOldest LiveChannelPublishProcessor LiveChannelPublishQueue SessionScoringProcessor sp.GetRequiredService IServiceScopeFactory SessionScoringQueue BackgroundService TryAddSingleton stoppingToken ReadAllAsync SingleReader"},{"u":"/docs/adr/052-background-job-execution.html#rationale","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Rationale","x":"- The host lifetime is the point. A BackgroundService is the only in-process shape the host can cancel and await. Everything else in the decision follows from wanting that. - The…","i":"BackgroundService TryEnqueue"},{"u":"/docs/adr/052-background-job-execution.html#trade-offs","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Trade-offs","x":"- In-process only. The queue does not survive a restart and does not span replicas. Accepted because every current job is either ephemeral (a lost broadcast is a missed UI…","i":"DropOldest Wait"},{"u":"/docs/adr/052-background-job-execution.html#related","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox, for work that must be durable and cross-process, and the post-commit deferral this relies on), ADR-008 (the broker, for cross-service work), ADR-014 (the…"},{"u":"/docs/adr/053-dual-registry-package-publishing.html","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#status","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-25). Amended (2026-07-25) to put the pre-decision Context statements in the past tense, to record the MMCA. ID prefix reservation as then-pending, to scope the…","i":"Directory.Build.props MMCA"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#context","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Context","x":"The fifteen MMCA.Common. packages have shipped to GitHub Packages since the first release. That was the right default while the framework had exactly one consumer group (this…","i":"MMCA.Common.API nuget.config local.props MMCA.Common totalHits package dotnet add"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#decision","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Decision","x":"Every release publishes to both registries, from the same tag, in the same workflow run. - release.yml keeps its existing dotnet nuget push to…","i":"github.repository_owner Directory.Build.props PackageProjectUrl PackageReadmeFile Description MMCA.Common PackageIcon PackageTags permissions release.yml README.md ivanball"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#rationale","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Rationale","x":"- The install line has to be true. Documentation that cannot be followed is worse than no documentation, because the reader concludes the project is broken rather than that the…","i":"MMCA"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#trade-offs","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A published version can never be withdrawn. nuget.org allows unlisting, not deletion. A bad release is now permanent public history, which raises the stakes on the release…","i":"release.yml ivanball"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#related","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Related","x":"ADR-016 (lockstep versioning: every package ships at one version, so both registries receive the same fifteen ids per release), ADR-038 (supply-chain provenance: the SBOM hard…"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#status","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-25). Amended (2026-07-28): Store's reconciliation sweep now derives from PeriodicBackgroundService, so the shared-loop and adoption paragraphs are rewritten and…","i":"PeriodicBackgroundService SafeDomainEventHandler TDomainEvent IUnitOfWork maxReplicas"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#context","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Context","x":"Checkout spans a boundary no transaction covers. CheckOutHandler commits the order insert, the cart transition and the atomic conditional stock decrements in one local…","i":"PaymentInitiated CheckOutHandler"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#decision","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Decision","x":"Multi-step workflows are choreographed sagas: each step raises a domain event, and the follow-up or compensating action lives in its own handler. A periodic reconciliation sweep…","i":"OrderPaymentFailedSagaHandler DbUpdateConcurrencyException PaymentReconciliationService OperationCanceledException OrderCancelledSagaHandler PeriodicBackgroundService Order.InventoryRestored SafeDomainEventHandler MarkInventoryRestored IServiceScopeFactory IDomainEventHandler MarkAsPaymentFailed"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#rationale","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Rationale","x":"- No two-phase commit is available, and none is wanted. Transactions are per data source and best-effort sequential (ADR-006), and an external payment provider cannot enlist in a…","i":"Order.InventoryRestored Order.Status SaveChanges Result catch"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#trade-offs","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Trade-offs","x":"- Inconsistency is bounded, not eliminated. Between the cancellation commit and the compensation commit, stock is held against a cancelled order. Between a dropped webhook and…","i":"PaymentInitiated RestoreInventory InventoryItem maxReplicas"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#related","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox delivery and retry this leans on for compensation redelivery; this record says what the redelivered handler must do), ADR-006 (which accepts \"no…","i":"RowVersion Result"},{"u":"/docs/adr/055-repository-and-specification-contract.html","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/055-repository-and-specification-contract.html#status","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-25). Revised 2026-08-01 (qualified the \"referenced nowhere\" claim about IEntityReader / IEntityQuerier: an ADC doc comment now names IEntityQuerier, though no…","i":"DependencyInjection.cs DependencyInjection EFReadRepository.cs IEntityQueryService QuerySpecification SessionsController Expression.Invoke EFReadRepository IEntityQuerier IRepository.cs IEntityReader ListAsync"},{"u":"/docs/adr/055-repository-and-specification-contract.html#context","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Context","x":"Every read an application handler performs has to come from somewhere, and the shape of that contract decides whether the module can still be lifted into its own service later…","i":"TIdentifierType IQueryable DbSet"},{"u":"/docs/adr/055-repository-and-specification-contract.html#decision","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Decision","x":"Data access is repository plus specification: interface-segregated read interfaces for the operations, expression-tree specifications for the predicates, and a build-failing…","i":"CrossSourceSpecification.BuildAsync IUnitOfWork.GetReadRepository OrdersByCustomerSpecification PublishedEventSpecification TableNoTrackingSingleQuery ParameterReplacer.Replace TableNoTrackingSplitQuery OwnedByUserSpecification publicSpecification.And EntityQueryService.cs ProductVariantService SpecificationComposer"},{"u":"/docs/adr/055-repository-and-specification-contract.html#rationale","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Rationale","x":"- A narrow interface is the enforcement, not a style preference. A handler that asks for IEntityReader cannot reach TableNoTracking, because the member is not on the interface.…","i":"GetProjectedAsync TableNoTracking IEntityReader IsSatisfiedBy AllowedFiles GetByIdAsync CountAsync IQueryable Criteria"},{"u":"/docs/adr/055-repository-and-specification-contract.html#trade-offs","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- The ISP split is guidance, not yet a wired dependency. (Superseded by the Revision (2026-08-21) below: the split now has real dependents in ADC (shipped) and Store (staged),…","i":"QuerySpecification Expression.Invoke ParameterReplacer Specification.cs ISpecification IEntityReader IUnitOfWork Criteria"},{"u":"/docs/adr/055-repository-and-specification-contract.html#related","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-007 and ADR-008 (the extraction promise the queryable ban exists to protect), ADR-015 (the fitness-function machinery that runs this rule and its per-repo maps), ADR-014 (the…","i":"SpecificationsDoNotNavigateToOtherEntities TIdentifierType"},{"u":"/docs/adr/055-repository-and-specification-contract.html#revision-2026-08-18","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Five changes, four of them widening the contract and one of them fixing a correctness defect. The decision this record states is unchanged: data access is still repository plus…","i":"NavigationMetadata.UnsupportedIncludes QueryFieldService.ApplySorting PushNotificationDTOProjection PushNotificationDTOProjector KeysetQueryBuilder.Compare PaginationTieBreakProperty EFReadRepositoryDecorator CrossSourceSpecification Error.InvalidEntityField SpecificationExtensions KeysetCollectionResult ExecuteProjectedAsync"},{"u":"/docs/adr/055-repository-and-specification-contract.html#revision-2026-08-21","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Revision (2026-08-21)","x":"Nothing in the contract changed; its consumers did. This revision records the first real adoption of the two surfaces this record had honestly flagged as unconsumed: the narrow…","i":"PublicConferenceVisibility SpecificationExtensions specification.Criteria ProductVariantService GetPageByCursorAsync GetProjectedAsync GetReadRepository AndSpecification IReadRepository IEntityQuerier IEntityReader specification"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#status","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28). Revised 2026-08-14: re-anchored the host, base-class and AppHost citations to their current lines; scoped the \"only @rendermode attributes\" enumeration to…","i":"InteractiveServer rendermode MudTable"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#context","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Context","x":"Both web applications are Blazor Web Apps: a static server-rendered (SSR) prerender pass produces the first HTML, then an interactive runtime takes over, either a Blazor Server…","i":"InteractiveAuto App.razor Authorize"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#decision","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Decision","x":"Run one render mode for the entire routable component tree, chosen at the application root, default InteractiveAuto, with prerendering left on and the resulting double fetch…","i":"AddInteractiveWebAssemblyComponents AddInteractiveWebAssemblyRenderMode AddInteractiveServerComponents AddInteractiveServerRenderMode RendererInfo.IsInteractive RenderMode.InteractiveAuto PersistentComponentState PrerenderFetchTimeoutMs InteractiveWebAssembly DataGridListPageBase OnParametersSetAsync RegisterOnPersisting"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#rationale","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Rationale","x":"- InteractiveAuto gets both halves without asking page authors to choose. The first visit gets the Server circuit's immediate interactivity while the WASM bundle downloads in the…","i":"InteractiveServer InteractiveAuto CatalogBrowse Authorize"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#trade-offs","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Trade-offs","x":"- Everything shared has to run in both runtimes. The WASM-compatibility layer rule (MMCA.Common.LayerEnforcement.targets:75-88) forbids the shared UI package from touching…","i":"RendererInfo.IsInteractive AddAdditionalAssemblies DataGridListPageBase MMCA.Common.UI.Web OnAfterRenderAsync InteractiveServer InteractiveAuto CatalogBrowse AddUIShared Program.cs Routes"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#related","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Related","x":"ADR-022 (reads the HttpOnly session cookie during the SSR prerender pass this decision keeps enabled), ADR-027 (flows one culture through the SSR to Server to WASM sequence this…","i":"InteractiveAuto"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#status","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28). Revised 2026-08-01: the diff now fails closed in both repos (the true is gone and MMCA.Store's build-and-test checkout sets fetch-depth: 0), so the…","i":"true"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#context","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Context","x":"ADR-030 decides who applies a migration: every service host runs DatabaseInitStrategy = Migrate and self-applies its pending EF Core migrations at startup as the sole migrator,…","i":"DatabaseInitStrategy containerapp DropColumn migrations adee5058 revision Migrate dotnet sqlcmd copy"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#decision","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Decision","x":"Schema changes follow expand/contract, and a CI step enforces the contract half. - Expand now, contract later, as a written rule. Adding nullable columns, new tables and new…","i":"OutboxMessages InboxMessages pull_request CreateIndex DropColumn Migrations DropIndex DropTable IsDeleted base_ref release added"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#rationale","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Rationale","x":"- Rollback is one-way for schema, so the check belongs where the drop is still cheap. The only moment a destructive migration can be reconsidered for free is the PR that adds it;…","i":"Down"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#trade-offs","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- Three operations, not a model of compatibility. AlterColumn narrowing a type or flipping a column to NOT NULL, DropForeignKey, DropPrimaryKey, DropSchema, RenameColumn and a…","i":"migrationBuilder.Sql DropForeignKey DropPrimaryKey RenameColumn AlterColumn DropSchema diff main true with git"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#related","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-030 (decides that each service self-applies its migrations at startup, which is precisely why a rolled-back revision meets the new schema; this ADR constrains what those…"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#status","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28; revised 2026-08-14 and 2026-08-18)."},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#context","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Context","x":"ADR-015 turned the architecture invariants into build-gating tests, and drew its own boundary explicitly: the fitness suite asserts \"structure / registration, not runtime…"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#decision","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Decision","x":"Ship the runtime conformance suites in the MMCA.Common.Testing package as abstract behavioral bases that each consuming host subclasses, and run every one of them against a host…","i":"ServiceInfoVersioningContractTestsBase SqlServerIntegrationTestFixtureBase MMCA.Common.Testing.Architecture ProductionHostApplicationFactory DecoratorPipelineOrderTestsBase ProblemDetailsContractTestsBase AssertProblemDetailsShapeAsync GracefulShutdownTestsBase AddApplicationDecorators ChangePreferencesCommand OpenApiContractTestsBase SecurityHeadersTestsBase"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#rationale","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Rationale","x":"- Runtime conformance is the half ADR-015 excluded. Structural rules answer \"is the code shaped correctly\"; these suites answer \"does the composed host behave correctly\". A host…","i":"Development Production"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#trade-offs","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in per host, exactly like ADR-015. The framework ships the suites; a host gets the gate only once someone writes the subclass. That is the same audit-the-inventory caveat,…","i":"CorePublicResources MinimumPathCount status title"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#related","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (the structural / registration fitness layer this complements; its stated non-goal, \"not runtime behavior\", is exactly this ADR's scope, and the two tiers ship as two…","i":"DecoratorPipelineOrderTestsBase ProblemDetailsContractTestsBase"},{"u":"/docs/adr/059-module-contract-and-composition.html","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records"},{"u":"/docs/adr/059-module-contract-and-composition.html#status","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28; revised 2026-08-14)."},{"u":"/docs/adr/059-module-contract-and-composition.html#context","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Context","x":"The framework's headline claim is that an application is built as a modular monolith and later extracted into services without rewriting business logic. ADR-008 states the…","i":"ModuleLoader"},{"u":"/docs/adr/059-module-contract-and-composition.html#decision","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Decision","x":"Make IModule the single composition contract, discover implementations by reflection, register them in topological dependency order, and represent a disabled module by stub…","i":"DisabledSessionBookmarkValidationService AppDomain.CurrentDomain.GetAssemblies DisabledEventLiveValidationService ModuleControllerFeatureProvider DisabledUserSalesExportService DisabledProductVariantService SalesUserDataExportSection ValidateRemoteDependencies InvalidOperationException Activator.CreateInstance AddUserDataExportSection DisabledCustomerService"},{"u":"/docs/adr/059-module-contract-and-composition.html#rationale","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Rationale","x":"- Reflection discovery keeps hosts out of the module registry business. A host calls one method and gets whatever modules its assembly graph contains; adding a module is a…","i":"RequiresDependencies RemoteDependencies appsettings.json Dependencies Modules true"},{"u":"/docs/adr/059-module-contract-and-composition.html#trade-offs","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Trade-offs","x":"- The AppDomain scan is the fragile default and the one everybody uses. The loader's own documentation warns that the AppDomain scan sees only assemblies already loaded, so a…","i":"ModuleConformanceTestsBase ValidateRemoteDependencies Activator.CreateInstance IBookmarkCountService RegisterDisabledStubs RequiresDependencies AddTypedGrpcClient Dependencies ModuleName Complete Register Enabled"},{"u":"/docs/adr/059-module-contract-and-composition.html#related","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Related","x":"ADR-008 (the extraction topology that consumes this model: \"a service is the monolith with one module enabled\" is a statement about ModuleLoader plus the Disabled stubs, cited…","i":"AddApplicationDecorators ModuleLoader"},{"u":"/docs/adr/060-performance-regression-gate.html","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records"},{"u":"/docs/adr/060-performance-regression-gate.html#status","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28). Revised 2026-08-01 (corrected the count in Trade-offs: the single ratio floor names two of the eight benchmarks, so six, not seven, are gated on…","i":"ci.yml"},{"u":"/docs/adr/060-performance-regression-gate.html#context","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Context","x":"Rubric section 12 asks for hot-path efficiency that is measured, not assumed (Website/docs-src/governance/ArchitectureEvaluationCriteria.md:355). MMCA.Common has a…","i":"IsSatisfiedBy"},{"u":"/docs/adr/060-performance-regression-gate.html#decision","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Decision","x":"Measure the hot-path suite on every code PR and verify the results against a committed baseline that carries two rule kinds: absolute allocation ceilings where the measurement is…","i":"ApplyFilters_ThreeMixedOperators IsSatisfiedBy_RecompileEachCall IsSatisfiedBy_CachedCompile allocationCeilingsBytes MMCA.Common.slnx PackageReference System.Text.Json BenchmarkDotNet MemoryDiagnoser fastBenchmark slowBenchmark Performance"},{"u":"/docs/adr/060-performance-regression-gate.html#rationale","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Rationale","x":"- A ratio is a property of the code; an absolute nanosecond count is a property of the runner. Both benchmarks in a floor run in the same process, on the same machine, in the…","i":"MemoryDiagnoser Specification TEntity TId"},{"u":"/docs/adr/060-performance-regression-gate.html#trade-offs","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The Short job cannot see small latency regressions. Three warmup and three iterations (ci.yml:360) give wide confidence intervals: enough for a 1000x floor and for counting…","i":"ApplyFilters release.yml changes main push"},{"u":"/docs/adr/060-performance-regression-gate.html#related","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (structural fitness functions, which explicitly stop at structure and registration; this is their runtime-cost counterpart), ADR-038 (the other build-gating control set,…"},{"u":"/docs/adr/061-runtime-secret-management.html","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records"},{"u":"/docs/adr/061-runtime-secret-management.html#status","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01; citations re-anchored 2026-08-14)."},{"u":"/docs/adr/061-runtime-secret-management.html#context","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Context","x":"A Container App can hold a credential two ways: as a literal value in the app's own secrets collection, or as a reference to a Key Vault secret that the platform resolves at…","i":"DefaultAzureCredential secrets"},{"u":"/docs/adr/061-runtime-secret-management.html#decision","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Decision","x":"Every production secret lives in Azure Key Vault and reaches the app as a keyVaultUrl secret reference resolved by a user-assigned managed identity. SQL authentication is staged…","i":"azureADOnlyAuthentication USE_MANAGED_IDENTITY_SQL useManagedIdentitySql hasSmtpPassword MMCA.Templates keyVaultUrl claude.yml Directory hasStripe secretRef existing Identity"},{"u":"/docs/adr/061-runtime-secret-management.html#rationale","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Rationale","x":"- A reference has one home; a literal has as many homes as it has consumers. Three vault secrets in each repo are referenced by more than one app: Redis and the broker by all…"},{"u":"/docs/adr/061-runtime-secret-management.html#trade-offs","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Trade-offs","x":"- One identity means vault-wide read for every app that carries it. A Key Vault Secrets User grant is scoped to the vault, so any app running as the shared identity can read…","i":"main.bicep EXTERNAL listKeys PROVIDER secrets CREATE secure unused FROM USER"},{"u":"/docs/adr/061-runtime-secret-management.html#related","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Related","x":"ADR-037 (directs a consumer to keep the field-encryption key in Key Vault but decides no delivery mechanism, and nothing wires that converter today, so no such secret exists in…"},{"u":"/docs/adr/062-slo-alerting-as-code.html","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/062-slo-alerting-as-code.html#status","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01). Revised 2026-08-18: Store's two operational extras (the outbox-dead-letter scheduled query rule and the outside-in Gateway availability web test with its…","i":"main"},{"u":"/docs/adr/062-slo-alerting-as-code.html#context","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Context","x":"ADR-041 standardized what the fleet emits: RED histograms off the CQRS pipeline, an outbox dead-letter counter, correlation ids, exporters, and the cost knobs that keep ingestion…"},{"u":"/docs/adr/062-slo-alerting-as-code.html#decision","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Decision","x":"Declare each consumer's SLO alerts as data in its Bicep template, materialize them as Log Analytics scheduled query rules, and make the alert-to-runbook pairing a build gate…","i":"EveryRunbookAlertSection_MapsToAProvisionedAlert SloAlertSpecs_AreDiscovered_GateIsNotVacuous ObservabilityConventionTestsBaseTests ObservabilityConventionTestsBase legacySloMetricAlertSpecs infra.OPERATIONS.md metricMeasureColumn alertEmailAddress MinimumAlertSpecs infra.main.bicep ResourceAssembly loadTextContent"},{"u":"/docs/adr/062-slo-alerting-as-code.html#rationale","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Rationale","x":"- Alerts as data, not as portal state. One array is reviewable in a PR, diffable across environments, and re-deployable; the rules, the workbook, and the notification channel are…","i":"enabled false"},{"u":"/docs/adr/062-slo-alerting-as-code.html#trade-offs","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- It is a text gate over IaC, not a check against deployed state. The base matches literal anchors and regexes in the template and headings in markdown. It proves the two files…","i":"sloAlertSpecs metricAlerts prefix key"},{"u":"/docs/adr/062-slo-alerting-as-code.html#related","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-041 (the telemetry this alerts on top of: it defines emission, instrumentation and cost knobs and stops before thresholds, severities and runbooks), ADR-009 (recovery…"},{"u":"/docs/adr/063-accessibility-conformance-gate.html","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#status","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01). Revised 2026-08-14: refreshed the E2ETestBase helper line anchors (explanatory comments were added above ScanGridAsync), the two consumer suite scan counts…","i":"ScanGridAsync E2ETestBase"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#context","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Context","x":"Accessibility was documented before it was enforced. The narrative guide (common-ACCESSIBILITY.md, rubric section 21) named WCAG 2.1 AA as the target for the shared…","i":"MMCA.Common.UI"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#decision","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Decision","x":"Ship WCAG 2.1 AA as a named, versioned test contract in MMCA.Common.Testing.E2E, assert it from the package's own workflow bases, and wire it as a required merge check and a…","i":"AxeOptions.Wcag21AaExceptMudPagerCombobox AssertNoAccessibilityViolationsAsync AccessibilityViolationException MMCA.Common.Testing.E2E ProfileManagementTests AxeOptions.Wcag21Aa PrimaryContrastText WarningContrastText GalleryAxeTestBase ErrorContrastText AxeRunOptions MudTablePager"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#rationale","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Rationale","x":"- A named constant is the contract. Putting the rule set in a shipped, referenced symbol rather than in each repo's test setup means \"what WCAG 2.1 AA means here\" has exactly one…","i":"ProfileManagementTestsBase UserRegistrationTestsBase UserLoginTestsBase"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#trade-offs","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- Best-practice rules are out of scope, deliberately. Findings axe would classify as best practice (and anything WCAG AAA) are not measured at all, so the gate can be green on a…","i":"Wcag21AaExceptMudPagerCombobox AccessibilityTests ScanGridAsync skipped success deploy"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#related","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (architecture fitness functions: the structural tier this parallels at the browser tier, and the same invariant-over-discipline posture), ADR-058 (runtime conformance…"},{"u":"/docs/adr/064-deploy-recency-gates.html","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records"},{"u":"/docs/adr/064-deploy-recency-gates.html#status","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01). Revised 2026-08-07: the MMCA.Helpdesk workflow inventory below was corrected (it also carries release-templates.yml, and its ci.yml runs two jobs, not…","i":"ci.yml"},{"u":"/docs/adr/064-deploy-recency-gates.html#context","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Context","x":"A production rollout in both deployed apps waits on a list of jobs in deploy.needs (MMCA.ADC/.github/workflows/deploy.yml:866, MMCA.Store/.github/workflows/deploy.yml:862). Most…","i":"deploy.needs"},{"u":"/docs/adr/064-deploy-recency-gates.html#decision","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Decision","x":"A production deploy is blocked not only on green tests but on proof of recency for out-of-band verification: three gates assert that a real drill, a real load run and a real…","i":"skip_freshness_gates skip_justification github.event_name workflow_dispatch FRESHNESS_DAYS workflow_runs deploy.needs release.yml foundation updated_at cancelled contents"},{"u":"/docs/adr/064-deploy-recency-gates.html#rationale","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Rationale","x":"- A proof with no expiry date is documentation, not a control. ADR-009 already required the drill to be recorded, and recording it was the honest half of the problem; a record…"},{"u":"/docs/adr/064-deploy-recency-gates.html#trade-offs","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Trade-offs","x":"- An unrelated stale proof blocks an unrelated deploy. A one-line hotfix does not ship when the monthly k6 cron did not fire, and the failure surfaces after merge: the gate job…","i":"deploy"},{"u":"/docs/adr/064-deploy-recency-gates.html#related","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Related","x":"ADR-009 (states the recovery objectives and requires that a restore be drilled and recorded; this record decides that a deploy is blocked on how recently that drill, and the…"},{"u":"/docs/adr/065-scaffolding-templates.html","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","x":"Status: Accepted (2026-08-02). Revised 2026-08-07: the staged analyzer delta relaxes three rules rather than one; mmca-module prints seven wire-ups rather than five, and a…","i":"Directory.Packages.props"},{"u":"/docs/adr/065-scaffolding-templates.html#context","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Context","x":"Build by hand is accurate and complete, and phases 1 through 6 of it are transcription work (common-BUILD-BY-HAND.md:96 through :1049). Its own instruction for the load-bearing…","i":"AddApplicationDecorators Directory.Packages.props Directory.Build.targets Directory.Build.props launchSettings.json IArchitectureMap MMCA.Templates editorconfig nuget.config global.json install WaitFor"},{"u":"/docs/adr/065-scaffolding-templates.html#decision","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Decision","x":"Ship a dotnet new template pack, MMCA.Templates, containing four templates: The template content is the MMCA.Helpdesk reference application itself, staged at pack time.…","i":"IntegrationEventContractTestsBase IntegrationEventContractTests SQLServerMigrationsAssembly WithSQLServerDataSource TreatWarningsAsErrors AddErrorResources appsettings.json IArchitectureMap Contoso.Support RequesterUserId MMCA.Templates MMCA.Helpdesk"},{"u":"/docs/adr/065-scaffolding-templates.html#rationale","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Rationale","x":"Deriving from the seed rather than maintaining a template tree is the whole design. A hand-maintained copy of a 12-project solution drifts within one release, and drift in a…","i":"MMCA.Common.Templates MMCA.Templates MMCA.Helpdesk sourceName Helpdesk install Tickets dotnet Ticket new"},{"u":"/docs/adr/065-scaffolding-templates.html#trade-offs","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two documented one-time fixups in every generated app, above (one of them covering all three relaxed rules). The alternative to the SA1210 half of the delta was moving every…","i":"IntegrationEventContractTestsBase IntegrationEventContractTests MMCA.Common copyOnly dotnet SA1210 using Fact new"},{"u":"/docs/adr/066-broker-transport-selection.html","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records"},{"u":"/docs/adr/066-broker-transport-selection.html#status","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07). Revised 2026-08-14 (source citations re-anchored; the ADC AppHost comment that used to say no WithBroker() was wired has been corrected in code, so the…","i":"WithBroker"},{"u":"/docs/adr/066-broker-transport-selection.html#context","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Context","x":"ADR-003 decides that integration events leave an aggregate through the outbox and are published by OutboxProcessor via IMessageBus, and it settles the dispatch question…","i":"OutboxProcessor IMessageBus"},{"u":"/docs/adr/066-broker-transport-selection.html#decision","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Decision","x":"Keep one IMessageBus abstraction with a three-value transport selector, choose the value at the deployment edge (never in application code), configure both broker transports…","i":"Bus.Factory.CreateUsingAzureServiceBus ResolveBrokerConnectionString MessageBus__ConnectionString ConnectionStrings__rabbitmq RootManageSharedAccessKey ConfigureBrokerTransport RetryMaxIntervalSeconds RetryMinIntervalSeconds builder.Configuration UseDelayedRedelivery UsingAzureServiceBus cfg.UseMessageRetry"},{"u":"/docs/adr/066-broker-transport-selection.html#rationale","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Rationale","x":"- The transport is a deployment fact, so it lives at the deployment edge. The only difference between a laptop and production is two environment variables set by the AppHost or…","i":"Listen Send"},{"u":"/docs/adr/066-broker-transport-selection.html#trade-offs","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two brokers means two behaviors to keep aligned. Configuration parity is enforced by one code path, but the products still differ (Service Bus supports delayed redelivery…","i":"MessageBus__Provider ConfigureEndpoints WithBroker Manage rabbit"},{"u":"/docs/adr/066-broker-transport-selection.html#related","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox that feeds IMessageBus; this ADR picks the transport underneath it), ADR-016 (the MassTransit v8 pin the emulator tier must work within, which is why the…","i":"IMessageBus Host"},{"u":"/docs/adr/067-ui-module-shell-composition.html","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records"},{"u":"/docs/adr/067-ui-module-shell-composition.html#status","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07)."},{"u":"/docs/adr/067-ui-module-shell-composition.html#context","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Context","x":"ADR-059 decided how a module plugs into the server: an IModule implementation is discovered by reflection, registered in topological order, and a host composes an application out…","i":"MMCA.Common.UI IModule Routes App"},{"u":"/docs/adr/067-ui-module-shell-composition.html#decision","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Decision","x":"Ship the application shell in the framework package and let each module plug into it by implementing IUIModule, resolved from DI as IEnumerable . - The contract is four members,…","i":"AdditionalAssemblies AppBarComponentTypes LayoutComponentTypes AuthorizeRouteView MapRazorComponents DynamicComponent UIModules.Select RedirectToLogin DeviceUIModule RequiredClaim TitleResource AddSingleton"},{"u":"/docs/adr/067-ui-module-shell-composition.html#rationale","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Rationale","x":"- One composition model across both tiers. A module already declares its server-side surface through IModule (ADR-059); declaring its UI surface through IUIModule means \"add a…","i":"AppBarComponentTypes LayoutComponentTypes AuthorizeView Components IUIModule IModule NavMenu"},{"u":"/docs/adr/067-ui-module-shell-composition.html#trade-offs","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Trade-offs","x":"- Assembly is required even when it carries no route. A host-only module that contributes only a layout component still has to return an assembly, which then joins…","i":"AddAdditionalAssemblies AdditionalAssemblies AuthorizeRouteView RequiredClaim MauiUIModule RequiredRole Program.cs IUIModule Assembly NavItems NavMenu page"},{"u":"/docs/adr/067-ui-module-shell-composition.html#related","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Related","x":"ADR-059 (the server-side IModule contract this mirrors in the presentation layer), ADR-056 (the render-mode strategy for the web heads, which decides how these components render…","i":"TitleResource IModule"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#status","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07)."},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#context","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Context","x":"A domain model has two kinds of small type: the identity of a thing, and a value the thing carries. ADR-048 recorded the identity half: identifiers stay primitives named through…","i":"decimal string"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#decision","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Decision","x":"Model a domain value that carries an invariant as an immutable record value object with a Result-returning factory; keep identifiers primitive (ADR-048). - One abstract record…","i":"PhoneNumberInvariants.EnsurePhoneNumberIsValid ArchitectureRules.DomainFactoriesReturnResult AddressInvariants.EnsureAddressLine1IsValid EmailInvariants.EnsureEmailIsValid NullablePhoneNumberValueConverter NullableEmailValueConverter EmailInvariants.MaxLength PhoneNumberValueConverter DataContractSerializer GetEqualityComponents DateTimeRange.Create ProductVariant.Price"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#rationale","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Rationale","x":"- The invariant belongs to the type, not to every caller. A string email can be validated in one handler and not the next; an Email cannot exist unvalidated, because the only…","i":"NullReferenceException Currency.None Money.Zero OwnsMoney record Result string Email Money"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#trade-offs","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Trade-offs","x":"- The pattern is not uniformly applied. Only three of the seven types have a companion Invariants class; the rest inline their checks. Only Money has a shipped owned-type helper,…","i":"InvalidOperationException DateTimeRange Currency.All PhoneNumber DateRange Money.Add operator Address OwnsOne Create Result string"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#related","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Related","x":"ADR-048 (the deliberate opposite call for identifiers: primitives behind aliases, wrapper structs rejected, because identifiers cross process boundaries constantly and carry no…","i":"Create Result"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#status","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07). Updated 2026-08-14: Store's adoption has landed and is live (its own dedicated storage account, gated on dataProtectionStorageReady), and the ADC call-site…","i":"dataProtectionStorageReady AddServiceDefaults"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#context","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Context","x":"ASP.NET Core's DataProtection default keeps the key ring in memory, per process. That is correct for a single-process host and wrong for a scaled-out one: every replica generates…","i":"DefaultAzureCredential maxReplicas"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#decision","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Decision","x":"Add one opt-in registration call, AddCommonDataProtection, that persists the key ring to a single Azure blob so every replica of a host shares one ring…","i":"Azure.Extensions.AspNetCore.DataProtection.Blobs KeyManagementOptions.XmlRepository System.Security.Cryptography.Xml AddCommonKeyVaultConfiguration DataProtection__BlobStorageUri DataProtection__KeyVaultKeyUri grantDataProtectionStorageRole PersistKeysToAzureBlobStorage ProtectKeysWithAzureKeyVault dataProtectionStorageReady AddCommonDataProtection IDataProtectionProvider"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#rationale","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Rationale","x":"- The key ring is the smallest thing that has to be shared. Sticky sessions would paper over the symptom while making a replica restart a mass sign-out, and a shared cache would…"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#trade-offs","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Trade-offs","x":"- The key ring is not encrypted at rest today. Gate 2 is implemented but configured nowhere, so the ring is protected by the container being private and the account grant being…","i":"AddCommonDataProtection AZURE_CLIENT_ID MMCA.ADC"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#related","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Related","x":"ADR-022 (the browser session cookies whose decryption this makes replica-independent, together with the antiforgery tokens the SSR pages mint), ADR-008 (the multi-host topology…","i":"DefaultAzureCredential"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#status","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07). Revised 2026-08-14 (source citations re-anchored; the consumer-repo facade claim narrowed to production code, with the controller-test exception recorded)."},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#context","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Context","x":"Every host in the workspace reads a dozen or more configuration sections: connection strings, SMTP, JWT key material, outbox tuning, message-bus provider, module enablement,…","i":"IOptions"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#decision","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Decision","x":"Bind every settings section through a validating chain that runs at startup, and expose a settings type through a read-only interface when it must be read above Infrastructure. -…","i":"IConnectionStringSettings IPushNotificationSettings ConnectionStringSettings PushNotificationSettings AddCommonAuthentication LoginProtectionSettings ValidateDataAnnotations DependencyInjection.cs LoginProtectionService CacheKeyPrefixOptions AddPushNotifications EntityControllerBase"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#rationale","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Rationale","x":"- A boot failure is cheaper than a first-use failure. A host that will not start is caught by the deployment, by a local dotnet run, or by CI. A host that starts and fails on the…","i":"Microsoft.Extensions.Options ValidateDataAnnotations EntityControllerBase IApplicationSettings ApplicationSettings IValidatableObject RepositoryFactory ValidateOnStart JwtSettings IOptions dotnet init"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#trade-offs","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- Nothing enforces it. There is no architecture fitness test asserting that a new AddOptions call carries ValidateDataAnnotations().ValidateOnStart(). The uniformity above is…","i":"ValidateDataAnnotations IValidatableObject IValidateOptions IOptionsMonitor ValidateOnStart JwtSettings AddOptions IOptions Value"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#related","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-025 (startup warm-up and readiness gating: this contract decides what happens before a host reaches that machinery), ADR-031 (feature flags read from configuration, whose…"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#status","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-12). Amended 2026-08-13: the composition-time string trade-off below was resolved in v1.147.0 by a deferred-resolution overload; see the updated trade-off…"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#context","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Context","x":"ADC's badge check-in feature (ADR-072) needs two things that look like one thing: an attendee's device has to show a QR code, and an organizer's device has to read one. They are…","i":"AddDeviceCapabilityDefaults NSCameraUsageDescription MMCA.Common.UI System.Drawing AddUIShared CAMERA"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#decision","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Decision","x":"Split the feature by what it actually depends on: QR display ships as a shared component, barcode scanning ships as an ADR-042 capability whose native half is opt-in per head. -…","i":"AddDeviceCapabilityDefaults DeviceInfo.Current.Platform MauiBarcodeScannerService NullBarcodeScannerService UseMauiDeviceCapabilities Permissions.RequestAsync ZXing.Net.Maui.Controls IBarcodeScannerService QrErrorCorrectionLevel ScanOnMainThreadAsync TaskCompletionSource MMCA.Common.UI.Maui"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#rationale","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Rationale","x":"- Rendering a QR is not a device concern, so making it one would have been ceremony. As a capability it would have needed an interface, a null fallback and a native override for…","i":"UseMauiDeviceCapabilities MMCA.Common.UI PngByteQRCode IsSupported MauiProgram null try"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#trade-offs","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Trade-offs","x":"- The scan page's strings were resolved at composition, not per call (resolved in v1.147.0). As shipped in v1.145.0, cancelText and cameraDescription were captured into the…","i":"UseCommonBarcodeScanner cameraDescription OnParametersSet MMCA.Common.UI IsSupported QrCodeImage cancelText QRCoder string catch false Func"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#related","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Related","x":"ADR-042 (the capability pattern this extends: contract in MMCA.Common.UI, native implementation in MMCA.Common.UI.Maui, override after AddUIShared), ADR-072 (the ADC feature that…","i":"MMCA.Common.UI.Maui MMCA.Common.UI AddUIShared"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#status","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). Amended (2026-08-14): ADC shipped two attendee-self-recorded scan surfaces (sponsor booth visits and room self check-in), a third CheckInScope, a sixth…","i":"CheckInScope"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#context","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Context","x":"ADC wanted two conference-day capabilities that turn out to be one mechanism. Organizers want to know who actually attended which session, which the schedule cannot tell them: a…"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#decision","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Decision","x":"AttendeeBadge (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Domain/Badges/AttendeeBadge.cs:17-24) is one row per user holding a single Guid Credential, minted on first…","i":"CheckInInvariants.EnsureTargetMatchesScope CheckInSettings.RoomCheckInGraceMinutes EngagementPermissions.CheckInManage CheckInProcessor.FindExistingAsync EngagementFeatures.SponsorVisits EngagementPointsEntryExportItem PointsActivityType.SponsorVisit EngagementFeatures.RoomCheckIn user_engagement_export.proto EngagementCheckInExportItem Engagement.SponsorVisits leaderboard_display_name"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#rationale","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Rationale","x":"- An opaque credential makes the server the only interpreter. A JWT or HMAC badge would verify offline, but the scanning device is online by necessity (it has to write a check-in…","i":"SessionCheckIn EventCheckIn Regenerate"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#trade-offs","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Trade-offs","x":"- The badge credential is a bearer value. Anyone who photographs an attendee's screen can be checked in as that attendee. The mitigations are that a badge scan is organizer-side,…","i":"DuplicateKeyDetection.IsDuplicateKey SetLeaderboardParticipationHandler GetLeaderboardHandler AttendeeCheckedIn Engagement.Points SessionFeedback SessionCheckIn activity_type IFeatureGated PointsAwarder QuestionAsked FeatureGate"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#related","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Related","x":"ADR-071 (the framework halves this consumes: the QR component on /my-badge and the scanner capability behind /check-in), ADR-003 (the outbox path AttendeeCheckedIn and the two…","i":"AttendeeCheckedIn EraseDisplayName"},{"u":"/docs/adr/073-multi-tenancy-model.html","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records"},{"u":"/docs/adr/073-multi-tenancy-model.html#status","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). The implementation lands in the MMCA.Common enterprise capability wave release, alongside the scheduler, audit trail, DSAR export, and CSV export work. It…","i":"ApplicationDbContext EFReadRepository AddMultiTenancy configuration"},{"u":"/docs/adr/073-multi-tenancy-model.html#context","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common already partitions data along two axes and neither of them is a tenant. ADR-006 partitions by source name (every entity resolves to a DataSourceKey(Engine, Name),…","i":"ApplySoftDeleteFilters SoftDeleteFilterName modelBuilder.Entity OnModelCreating HasQueryFilter DataSourceKey OnConfiguring TenantId clrType Engine filter Where"},{"u":"/docs/adr/073-multi-tenancy-model.html#decision","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Decision","x":"Ship shared-schema tenancy as a second named query filter, with per-tenant database routing as a configuration override on the same source key, both opt-in and both inert until a…","i":"IPhysicalDbContextFactory.Create CosmosDbContext.OnModelCreating TenantSaveChangesInterceptor UseCommonMiddlewarePipeline TenantResolutionMiddleware CrossTenantWriteException DesignTimeDbContextHelper SoftDeletedUserMiddleware ITenantContext.SetTenant CachingCommandDecorator CorrelationIdMiddleware InitializeDatabaseAsync"},{"u":"/docs/adr/073-multi-tenancy-model.html#rationale","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Rationale","x":"- A query filter is the only place the rule cannot be forgotten. Per-handler Where clauses are correct until the tenth handler, and the tenth handler is a data leak rather than a…","i":"IgnoreQueryFilters DataSourceKey ICacheService RequireTenant Where"},{"u":"/docs/adr/073-multi-tenancy-model.html#trade-offs","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Reads are on discipline where writes are on an invariant. A consumer calling EF's own parameterless IgnoreQueryFilters() on a raw Table surface drops the tenant filter along…","i":"ApplicationLayer_DoesNotUseRawQueryableSurfaces DefaultSqlServerDbContextFactory TenantSaveChangesInterceptor IgnoreQueryFilters ICacheService ITenantEntity tenant_id Table"},{"u":"/docs/adr/073-multi-tenancy-model.html#related","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Related","x":"ADR-006 (the source-name axis this composes with: an override re-points a DataSourceKey without changing it, and the per-source outbox this record drains once per tenant),…","i":"IgnoreQueryFilters CosmosDbContext TenancySettings DataSourceKey tenantId TenantId string"},{"u":"/docs/adr/074-recurring-job-scheduler.html","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records"},{"u":"/docs/adr/074-recurring-job-scheduler.html#status","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13; revised 2026-08-14, 2026-08-18). The implementation lands in the MMCA.Common \"enterprise capability wave\" release and is opt-in: a host calls…","i":"AddScheduledJobs configuration"},{"u":"/docs/adr/074-recurring-job-scheduler.html#context","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Context","x":"The framework had two kinds of background work and neither of them is a schedule. OutboxProcessor…","i":"PeriodicBackgroundService OutboxProcessor"},{"u":"/docs/adr/074-recurring-job-scheduler.html#decision","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Decision","x":"A persistent job store plus a single-runner claim lease, reusing the exact idiom the outbox proved. The outbox claims a batch with an ExecuteUpdateAsync that sets LockedUntil and…","i":"DesignTimeDbContextOptions.EnableScheduler ScheduledJobOverrideSettings.Cron DesignTimeDbContextHelper PeriodicBackgroundService Directory.Packages.props EnsurePermissionRegistry ValidateDataAnnotations PollingIntervalSeconds SchedulerSettings.Jobs SyncRegistrationsAsync ResolveCronExpression AuditTrailCleanupJob"},{"u":"/docs/adr/074-recurring-job-scheduler.html#rationale","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Rationale","x":"- The lease is already proven under production load. Multi-replica correctness for recurring work is the hard part, and it was solved once for the outbox: an atomic claim update,…","i":"AddScheduledJobs IUnitOfWork LastRunOn NextRunOn DateTime"},{"u":"/docs/adr/074-recurring-job-scheduler.html#trade-offs","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A polling loop is not a real-time scheduler. Worst-case start lag is one polling interval, 30 seconds at the default, so sub-minute precision is not on offer. A job that must…","i":"LeaseSeconds"},{"u":"/docs/adr/074-recurring-job-scheduler.html#related","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox whose claim-lease idiom and smart wait this reuses verbatim, and whose at-least-once posture it inherits along with the idempotency obligation on job bodies),…","i":"SchedulerSettings SchedulerMetrics OutboxMetrics"},{"u":"/docs/adr/075-audit-trail.html","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records"},{"u":"/docs/adr/075-audit-trail.html#status","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13; corrected 2026-08-14: the adoption sweep and the ApplicationDbContext line citations). The implementation lands in the MMCA.Common \"enterprise capability…","i":"ApplicationDbContext IAuditedEntity AddAuditTrail configuration"},{"u":"/docs/adr/075-audit-trail.html#context","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Context","x":"The framework already answers \"who touched this row last\". Every AuditableBaseEntity carries CreatedOn/By and LastModifiedOn/By, stamped by AuditSaveChangesInterceptor on the way…","i":"AuditSaveChangesInterceptor AuditableBaseEntity SaveChangesAsync LastModifiedBy"},{"u":"/docs/adr/075-audit-trail.html#decision","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Decision","x":"AuditTrailSaveChangesInterceptor (Infrastructure Persistence/AuditTrail/) joins the interceptors ApplicationDbContext.OnConfiguring already passes to…","i":"ApplicationDbContext.OnModelCreating ApplicationDbContext.OnConfiguring DomainEventSaveChangesInterceptor AuditTrailSaveChangesInterceptor optionsBuilder.AddInterceptors TenantSaveChangesInterceptor AuditSaveChangesInterceptor DesignTimeDbContextHelper PeriodicBackgroundService PiiRedactor.RedactedToken DiscardAbandonedCapture DependencyInjection.cs"},{"u":"/docs/adr/075-audit-trail.html#rationale","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Rationale","x":"- IAuditableEntity is a statement about a business row, and an audit row is not one. The interface means \"this row stamps who created and last modified it and participates in…","i":"IAuditableEntity LastModifiedBy IScheduledJob OutboxMessage TenantId Pii"},{"u":"/docs/adr/075-audit-trail.html#trade-offs","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Write amplification is real and it is on the caller's latency path. An entity with twenty changed properties writes twenty rows inside the caller's transaction, so an audited…","i":"IAuditTrailReader AddAuditTrail RetentionDays Pii"},{"u":"/docs/adr/075-audit-trail.html#related","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the same-transaction write this copies wholesale, including the retry-discard and the Add-only mutation rule), ADR-005 (soft-delete, [Pii] and erasure: why the trail…","i":"AuditTrailSettings RowVersion TenantId Add Pii"},{"u":"/docs/adr/076-data-subject-export.html","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/076-data-subject-export.html#status","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). Revised 2026-08-14 (the API-surface section corrected to the shipped mechanism, an abstract DataExportControllerBase a subclass mounts, not an…","i":"ExportUserDataHandlerBase DataExportControllerBase IUserDataExportSection"},{"u":"/docs/adr/076-data-subject-export.html#context","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Context","x":"A data-subject access request is a legal obligation with a clock on it: the person asks for a copy of the personal data held about them, and the operator has a deadline to hand…","i":"DeleteUserHandlerBase UserOwnershipRule IAnonymizable"},{"u":"/docs/adr/076-data-subject-export.html#decision","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Decision","x":"The framework takes the part that is the same in both apps; the app keeps the part that is not. A consumer's export handler becomes a subclass that supplies a role test and a set…","i":"AuthorizationPolicies.RequireAuthenticated EntitiesWithPiiImplementAnonymizable UserOwnershipRule.CheckOwnership AuditableAggregateRootEntity IUserEngagementExportService AddNotificationControllers PrivacyFeatures.DataExport AuthenticationServiceBase ExportUserDataHandlerBase DataExportControllerBase PiiEntitiesAreExportable IUserDataExportSection"},{"u":"/docs/adr/076-data-subject-export.html#rationale","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Rationale","x":"- The two halves of a handler have different owners. The ownership gate, the aggregate load, the fan-out, the per-section catch and the envelope are the same decisions in both…","i":"IUserEngagementExportService IUserSalesExportService ExportUserDataQuery UserOwnershipRule User"},{"u":"/docs/adr/076-data-subject-export.html#trade-offs","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- Best-effort degradation can return a quietly incomplete package. Available = false is the only signal, and nothing forces a caller, a UI, or the subject to read it. A section…","i":"DataExportControllerBase UserDataExportDTO UserOwnershipRule CurrentUserId FeatureGate Available false"},{"u":"/docs/adr/076-data-subject-export.html#related","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (the erasure half of the same privacy obligation, whose IAnonymizable opt-in and [Pii] guard are this contract's mirror: one erases what the other copies), ADR-033 (the…","i":"PiiEntitiesAreExportable UserOwnershipRule IAnonymizable FeatureGate Result Pii"},{"u":"/docs/adr/077-hybridcache-substrate.html","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","i":"ICacheService"},{"u":"/docs/adr/077-hybridcache-substrate.html#status","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). Amends ADR-026: Tier 1's substrate gains a third implementation beside MemoryCacheService and DistributedCacheService. It is opt-in through…","i":"OutputCacheEvictionRequested DistributedCacheService MMCA.Common.OutputCache AddCommonHybridCache MemoryCacheService"},{"u":"/docs/adr/077-hybridcache-substrate.html#context","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Context","x":"ADR-026 settled Tier 1 as one abstraction (ICacheService) over two implementations chosen at startup: in-process memory when no real IDistributedCache is present, Redis…","i":"Microsoft.Extensions.Caching.Hybrid ICacheService.IncrementAsync StackExchangeRedisCache IDistributedCache ICacheService HybridCache WRONGTYPE Result INCR"},{"u":"/docs/adr/077-hybridcache-substrate.html#decision","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Decision","x":"Ship HybridCacheService as a third ICacheService implementation, opt-in per host, under a disjoint keyspace. This is the structural rule the design is built around, and…","i":"HybridCacheEntryFlags.DisableUnderlyingData Microsoft.Extensions.Caching.Hybrid CacheOptions.DefaultDuration HybridCache.GetOrCreateAsync HybridCache.RemoveByTagAsync MMCA.Common.Infrastructure Directory.Packages.props DistributedCacheService DisableLocalCacheWrite IConnectionMultiplexer CachingQueryDecorator DisableLocalCacheRead"},{"u":"/docs/adr/077-hybridcache-substrate.html#rationale","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Rationale","x":"- The disjoint keyspace is the decision; everything else is implementation. Rather than trusting a second implementation to write a shape compatible with the first, this record…","i":"DisableUnderlyingData LocalCacheExpiration IncrementAsync GetAsync"},{"u":"/docs/adr/077-hybridcache-substrate.html#trade-offs","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Invalidation does not reach other replicas' L1 immediately. A remove evicts the L2 entry and the calling replica's L1; every other replica keeps its copy for up to…","i":"AddCommonHybridCache LocalCacheExpiration GetOrCreateAsync IncrementAsync ICacheService RemoveAll"},{"u":"/docs/adr/077-hybridcache-substrate.html#related","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Related","x":"ADR-026 (amended by this record: its Tier 1 substrate gains a third implementation, its 30-second default TTL becomes the local-cache bound as well, its prefix-invalidation model…","i":"IncrementAsync GetAsync"},{"u":"/docs/adr/078-csv-export-endpoint.html","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records"},{"u":"/docs/adr/078-csv-export-endpoint.html#status","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13; revised 2026-08-18). The implementation lands in the MMCA.Common \"enterprise capability wave\" release. Unlike the wave's other features this one is NOT…","i":"EntityControllerBase"},{"u":"/docs/adr/078-csv-export-endpoint.html#context","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Context","x":"The request is \"export what you filtered\". The generic entity surface of ADR-034 already accepts a full query vocabulary on the paged route…","i":"EntityQueryPipeline.MaxUnboundedResultLimit context.CacheVaryByRules.QueryKeys options.ReturnHttpNotAcceptable PublicEndpointOutputCachePolicy ReturnHttpNotAcceptable QueryFilterModelBinder IAsyncEnumerable OutputFormatter sortDirection sortColumn Accept AddAPI"},{"u":"/docs/adr/078-csv-export-endpoint.html#decision","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Decision","x":"EntityControllerBase gains a virtual [HttpGet(\"export\")] ExportAsync(...) action (Source/Presentation/MMCA.Common.API/Controllers/EntityControllerBase.cs). It accepts the same…","i":"QueryFieldService.ShapeCollectionData IPhysicalDbContextFactory.Create ApplicationSettings.MaxPageSize IEntityQueryService.GetAllAsync UnhandledResultFailureFilter JsonNamingPolicy.CamelCase ExportRowLimitHeaderName OpenApiContractTestsBase MaxUnboundedResultLimit QueryFilterModelBinder IEntityControllerBase EntityControllerBase"},{"u":"/docs/adr/078-csv-export-endpoint.html#rationale","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Rationale","x":"- A route is an unambiguous request; an Accept header is a preference. Given a cache policy that ignores Accept and a pipeline configured to never return 406, a client that…","i":"OutputFormatter Accept"},{"u":"/docs/adr/078-csv-export-endpoint.html#trade-offs","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Trade-offs","x":"- Every derivative gains a bulk read whether its owner wanted one or not. The only gate is the controller's existing authorization posture. A resource that was safe to page 20…","i":"GetExportSpecification MaxExportRows ExportAsync MaxPageSize Accept Skip Take"},{"u":"/docs/adr/078-csv-export-endpoint.html#related","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Related","x":"ADR-034 (the generic entity surface and query contract this extends, and the MaxUnboundedResultLimit ceiling that forced the page loop), ADR-040 (the output-cache policy whose…","i":"MaxUnboundedResultLimit Accept Result"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#status","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14). Revised 2026-08-19 (refreshed the WebApplicationBuilderExtensions.cs cross-reference anchor, which moved to :555). Revised 2026-08-21: the order became…","i":"MiddlewarePipelineBuilder.CreateDefault WebApplicationBuilderExtensions.cs MiddlewarePipelineOrderTestsBase"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#context","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Context","x":"In ASP.NET Core, middleware order is behavior, not style: a rate limiter placed before authentication partitions every request as anonymous, an HTTPS redirect placed in front of…","i":"TenantResolutionMiddleware SoftDeletedUserMiddleware UseAuthentication HttpContext.User"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#decision","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Decision","x":"Ship the edge as one ordered pipeline in the framework, UseCommonMiddlewarePipeline (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:46), and…","i":"MiddlewarePipelineBuilder.CreateDefault MiddlewarePipelineOrderTestsBase DecoratorPipelineOrderTestsBase UseCommonRequestLocalization MiddlewarePipelineStepNames UseCommonMiddlewarePipeline TenantResolutionMiddleware MiddlewarePipelineBuilder SoftDeletedUserMiddleware MiddlewarePipelineStep OidcDiscoveryEndpoint SoftDeletedUserFilter"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#rationale","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Rationale","x":"- Order is behavior, so it belongs to the framework, not to each host. Four of the adjacencies above fail silently when reversed: the limiter stops limiting, the tenant resolver…","i":"Program.cs"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#trade-offs","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two of this record's original costs are retired (2026-08-21). As accepted, nothing froze the order (no test referenced the method; the adjacencies were protected by comments…","i":"MiddlewarePipelineOrderTestsBase MiddlewarePipelineStepNames MapOidcDiscoveryEndpoint UseCommonSecurityHeaders PreForwardedCapture HttpContext.Items KnownIPNetworks InsertBefore KnownProxies PreForwarded Controllers jwks_uri"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#related","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Related","x":"ADR-014 (the in-process sibling: one fixed decorator order for commands and queries), ADR-019 (depends on forwarded headers before the limiter and on the limiter after…"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#status","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#context","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Context","x":"Both production apps deploy to Azure Container Apps from a single deploy.yml job on push to main, and every gate runs before anything rolls out: the deploy job waits on…","i":"deploy.yml foundation deploy main"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#decision","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Decision","x":"Roll out one revision at a time, verify it from outside, and auto-revert the image only when the verification fails. - Single-revision rollout. Every container app runs…","i":"activeRevisionsMode rollback_failed containerapp createdTime Provisioned pipefail revision rollback failure sqlcmd probe Smoke"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#rationale","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Rationale","x":"- ARM success is the wrong success signal. The smoke gate converts \"the control plane accepted the template\" into \"the fleet answers requests\", which is the only claim a deploy…","i":"deploy"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#trade-offs","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Trade-offs","x":"- Schema is never rolled back, so a bad migration is fix-forward only. The image reverts and the database does not, so the previous release resumes against the new schema. This…","i":"rollback_failed Provisioned revision APPS copy"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#related","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Related","x":"ADR-057 (built on this model: revision-only rollback is why every migration must be backward compatible one release back), ADR-030 (startup migration as sole migrator, the reason…"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#status","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#context","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Context","x":"Both deployed apps run a deliberately small production footprint: every Container App is declared with maxReplicas: 2 and every SQL database with the Basic tier…","i":"maxReplicas Basic"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#decision","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Decision","x":"The cost baseline is asserted by a read-only reusable workflow that both runs weekly and sits in deploy.needs, so an un-reverted scale-up blocks the next production deploy. - One…","i":"properties.template.scale.maxReplicas BASELINE_MAX_REPLICAS AZURE_RESOURCE_GROUP github.event_name workflow_dispatch workflow_call deploy.needs environment release.yml main.bicep production MMCAStore"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#rationale","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Rationale","x":"- Configuration drift is the leading indicator; spend is the lagging one. The budget notification fires at 80% of actual spend, after the money is gone, and names a number rather…","i":"workflow_call deploy.needs maxReplicas deploy.yml sku.tier"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#trade-offs","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- A legitimate scale-up blocks deploys until the baseline is edited. Standing up extra capacity for a real event and then shipping a fix during it requires a pull request against…","i":"BASELINE_MAX_REPLICAS skip_freshness_gates skip_justification workflow_dispatch deploy.needs maxReplicas deploy.yml sku.tier Standard deploy Basic write"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#related","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-064 (the sibling deploy-precondition record, which decides the three proof-of-recency gates and enumerates this one only in passing; its break-glass input does not apply…","i":"deploy.needs"},{"u":"/docs/adr/082-two-tier-cors-posture.html","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records"},{"u":"/docs/adr/082-two-tier-cors-posture.html#status","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/082-two-tier-cors-posture.html#context","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Context","x":"Both deployed applications put a YARP gateway in front of per-module service hosts (ADR-008), and the browser and MAUI clients talk to the gateway origin while the services…"},{"u":"/docs/adr/082-two-tier-cors-posture.html#decision","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Decision","x":"Ship two cross-origin policies from the framework: an allow-listed one for service hosts and a deliberately broader one for gateways. - Service hosts register two named policies…","i":"CorsPolicyAllowSpecificOrigins app.Environment.IsDevelopment UseCommonMiddlewarePipeline Cors__AllowedOrigins__0 _allowSpecificOrigins AddCommonGatewayCors CorsPolicyAllowAll UseAuthentication AddDefaultPolicy AllowCredentials IHostEnvironment AllowAnyHeader"},{"u":"/docs/adr/082-two-tier-cors-posture.html#rationale","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Rationale","x":"- A proxy cannot allow-list what it does not own. The gateway has no controllers and no knowledge of which headers the fronted services accept, so a header allow-list there would…","i":"UseCommonMiddlewarePipeline AllowCredentials IHostEnvironment AllowAnyOrigin AddCommonCors UseCors"},{"u":"/docs/adr/082-two-tier-cors-posture.html#trade-offs","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Trade-offs","x":"- The gateway policy is broad on two of three axes. Any header and any method are accepted for an allow-listed origin. The origin list is the only lever there, so a mistake in…","i":"ProductionHostApplicationFactory IHostEnvironment.IsDevelopment configuration.GetSection ValidateOnStart UseEnvironment AddCommonCors UseCors string Get"},{"u":"/docs/adr/082-two-tier-cors-posture.html#related","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Related","x":"ADR-079 (the shared middleware pipeline whose fixed order places the environment-selected CORS policy between routing and authentication), ADR-008 (the gateway plus per-module…"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#status","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#context","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Context","x":"ADR-003 decides how a domain event moves: captured into the outbox inside SaveChangesAsync, dispatched in-process after commit, or published to the broker when it is an…","i":"SaveChangesAsync SessionChanged SessionCreated SessionDeleted Changed Created Deleted Session Entity"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#decision","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Decision","x":"Every generic CRUD lifecycle transition of an entity raises one event type for that entity, carrying a DomainEntityState discriminator; handlers filter on State. - One base…","i":"ProductVariantPriceChanged TicketChangedAuditHandler ProductVariantSkuChanged ShoppingCartItemChanged SessionQuestionChanged ShoppingCartCheckedOut ProductVariantRemoved SessionCreatedHandler BaseIntegrationEvent ProductVariantAdded EntityChangedEvent DomainEntityState"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#rationale","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Rationale","x":"- One type per entity is one subscription surface. A subscriber declares interest in the entity, then decides which transitions matter, instead of the container deciding for it…","i":"SessionChanged SessionCreated SessionDeleted OrderPaid"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#trade-offs","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Trade-offs","x":"- Every selective handler pays a filter. A handler that cares about one transition has to open with a State guard and return (SessionCreatedHandler.cs:17-18 is the shape to…","i":"EntityChangedEvent PointsEntryChanged BaseDomainEvent LivePollChanged LivePollStatus Unchanged Added State TId"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#related","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (how these events are captured and dispatched; this ADR decides only their shape), ADR-010 (schema versioning for the discriminator once it crosses a service boundary),…","i":"MessageId"},{"u":"/docs/adr/084-stripe-webhook-ingress.html","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#status","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/084-stripe-webhook-ingress.html#context","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Context","x":"Four ADRs already cover how a message crosses a boundary in this workspace. ADR-003 decides how an event leaves a service (outbox, at-least-once). ADR-021 decides how a…","i":"Warning"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#decision","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Decision","x":"Treat third-party webhook ingress as its own contract with two halves: an acceptance-coded endpoint and a self-registering, self-provisioning endpoint registration at startup. -…","i":"StripeWebhookRegistrationService EventUtility.ValidateSignature payment_intent.payment_failed AddModuleSalesInfrastructure SignatureVerificationFailed StripeWebhookSecretProvider checkout.session.completed throwOnApiVersionMismatch checkout.session.expired HttpContext.Request.Body EventUtility.ParseEvent Stripe__WebhookBaseUrl"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#rationale","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Rationale","x":"- The caller's protocol decides the response vocabulary. Stripe reads a status code as \"keep retrying\" or \"stop\", not as \"this succeeded\" or \"this failed\". Mapping every…","i":"Critical Warning"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#trade-offs","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A startup service that writes to a live third-party account. Booting a Sales replica creates and deletes webhook endpoints in the real Stripe account…","i":"StripeWebhookRegistrationService PaymentReconciliationService PaymentsController WebhookBaseUrl SecretKey Critical Warning"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#related","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (outbound at-least-once delivery, the other end of the same family), ADR-021 (broker-side inbound dedup, which never sees a webhook), ADR-017 (client-supplied idempotency…"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#status","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Revisits ADR-048, which stays Accepted and unchanged in substance: the aliases remain the identifier model. What changes is the shape of the deferral.…"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#context","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Context","x":"ADR-048 decided that every entity identity is a primitive named through a per-module global using {Entity}IdentifierType = ... alias, and recorded the cost in one line of…","i":"SpeakerIdentifierType UserIdentifierType StronglyTypedId IdentifierType System.Guid Entity global Source using Vogen int"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#decision","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Decision","x":"Keep the aliases. The wrapper-struct alternative is evaluated in this record, priced, and deferred again, this time against explicit triggers. Inside a module an identifier is…","i":"SessionIdentifierType SponsorIdentifierType UserIdentifierType checkedInByUserId IEntityDTOMapper TIdentifierType ValueConverter JsonConverter BaseEntity sessionId sponsorId IBaseDTO"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#rationale","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Rationale","x":"- The cost is paid once and the benefit accrues per defect avoided, and the defect count is currently zero. No production incident in any of the four repos has been traced to a…","i":"System.Text.Json Guid int"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#trade-offs","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The exposure is unmitigated, not reduced. This record buys no safety whatsoever. Every transposition ADR-048 could not catch is still uncatchable today, and the CheckIn…","i":"CheckIn.Create CheckIn int"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#related","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Related","x":"ADR-048 (the decision this record revisits and upholds; its Status now points here), ADR-068 (the deliberate opposite case: domain values carry invariants and therefore do get…"},{"u":"/docs/adr/086-process-manager-deferred.html","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records"},{"u":"/docs/adr/086-process-manager-deferred.html#status","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18) as a documented deferral. Nothing ships with this record: no state machine, no correlation store, no new package. What ships is the shape the coordinator…"},{"u":"/docs/adr/086-process-manager-deferred.html#context","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Context","x":"ADR-054 decided how this workspace achieves cross-boundary consistency without two-phase commit: choreography. Each step of a workflow raises a domain event, each compensating…","i":"PaymentReconciliationService PeriodicBackgroundService SagaStateMachineInstance MassTransitStateMachine Order.InventoryRestored InMemorySagaRepository Order.Status SaveChanges Source ISaga"},{"u":"/docs/adr/086-process-manager-deferred.html#decision","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Decision","x":"Defer the process manager, and record its shape so the deferral is a design decision rather than an omission. A durable multi-step workflow coordinator in this workspace is a…","i":"MassTransit.Azure.ServiceBus.Core MassTransitStateMachine MassTransit.RabbitMQ CorrelationId MassTransit InProcess TInstance Result"},{"u":"/docs/adr/086-process-manager-deferred.html#rationale","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Rationale","x":"- Choreography is genuinely correct for the workflow that exists. This is not a case of the simpler option being tolerated. Checkout's saga state is two fields on Order, and an…","i":"Order"},{"u":"/docs/adr/086-process-manager-deferred.html#trade-offs","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The first workflow to hit the trigger pays the full cost at once, under whatever deadline made it appear. Deferral moves the work onto the critical path of the feature that…","i":"SQLServerDbContext"},{"u":"/docs/adr/086-process-manager-deferred.html#related","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Related","x":"ADR-054 (the accepted mechanism this record defers an alternative to: choreographed compensation, the persisted aggregate marker, and the reconciliation sweep that would remain…"},{"u":"/docs/adr/087-broker-poison-message-handling.html","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records"},{"u":"/docs/adr/087-broker-poison-message-handling.html#status","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Amends ADR-009: the outbox's broker publish gains a circuit breaker, which is the first resilience policy this workspace applies to something other than an…"},{"u":"/docs/adr/087-broker-poison-message-handling.html#context","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Context","x":"Delivery in this workspace has always been at-least-once with retries on both legs: the outbox retries a failed publish with jittered exponential backoff and eventually…","i":"rabbitmq_delayed_message_exchange DeadLetterRetentionDays"},{"u":"/docs/adr/087-broker-poison-message-handling.html#decision","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Decision","x":"Three changes, each scoped to one failure: second-level redelivery configured per transport, a fault consumer with its own meter, and a circuit breaker around the outbox's broker…","i":"RegisterIntegrationEventConsumer settings.EnableDelayedRedelivery FaultIntegrationEventConsumer OperationCanceledException RedeliveryIntervalsSeconds broker.circuit.open.count BuildRedeliveryIntervals cfg.UseDelayedRedelivery ConfigureBrokerTransport EnableDelayedRedelivery BrokenCircuitException fault.FaultedMessageId"},{"u":"/docs/adr/087-broker-poison-message-handling.html#rationale","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Rationale","x":"- The transport asymmetry follows a real capability difference, not a preference. RabbitMQ needs a plugin the dev container lacks; Service Bus does not. A single default would be…","i":"BrokenCircuitException true"},{"u":"/docs/adr/087-broker-poison-message-handling.html#trade-offs","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Trade-offs","x":"- Delayed redelivery is off where the plugin problem lives. RabbitMQ is the local transport and also a plausible self-hosted production transport; both get default-off, so the…","i":"RegisterIntegrationEventConsumer RedeliveryIntervalsSeconds broker.fault.count MMCA.Common.Aspire BrokerMetrics internal"},{"u":"/docs/adr/087-broker-poison-message-handling.html#related","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox publish leg this breaker wraps, and the retry, jittered backoff and dead-lettering that BrokenCircuitException reuses unchanged), ADR-066 (the transport…","i":"RedeliveryIntervalsSeconds BrokenCircuitException MMCA.Common.Broker MMCA.Common.Outbox MMCA.Common.Cqrs"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#status","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Extends ADR-019 with a fourth, edge-tier layer whose posture is the deliberate opposite of the service tier's authenticated-only global limiter; nothing in…"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#context","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Context","x":"ADR-008 made the Gateway the only client entry point and gave it three jobs: the route-to-service map, CORS, and forwarding the caller's Authorization header. Nothing was added…","i":"CorrelationIdMiddleware AddCommonRateLimiting MMCA.Common.Aspire MMCA.Common.API Authorization"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#decision","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Decision","x":"Ship a gateway edge kit in a Gateway namespace inside MMCA.Common.Aspire, owning exactly three responsibilities, and record three more as deliberately declined. 1. Correlation is…","i":"PartitionedRateLimiter.CreateChained AddGatewayDownstreamHealthChecks RateLimitPartition.GetNoLimiter GatewayCorrelationMiddleware GatewayRateLimitingSettings HttpContext.TraceIdentifier Connection.RemoteIpAddress Validator.ValidateObject ValidateDataAnnotations AddGatewayRateLimiting GlobalConcurrencyLimit UseGatewayCorrelation"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#rationale","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Rationale","x":"- The edge is the only place that sees a request exactly once. That is what makes ensure-at-the-edge correct and mint-per-service wrong: not that the service version is broken,…","i":"Ready Live"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#trade-offs","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The limiter closes over an eagerly-bound copy of the settings (GatewayRateLimitingExtensions.cs:154, consumed at :164-172), so an IOptionsMonitor reload never reaches it.…","i":"BypassPathPrefixes IOptionsMonitor PermitLimit IOptions"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#related","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Related","x":"ADR-008 (the record that made the Gateway the only entry point and gave it routing, CORS and auth forwarding; this is the first record to add cross-cutting behavior to it),…"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#status","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Amends ADR-008: that record gave the Gateway the route-to-service map and expressed it as code (a list of MapForwarder calls). The map itself is unchanged;…","i":"MapForwarder ReverseProxy"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#context","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Context","x":"Before this record, both gateways built their route table by hand, in code. ADC made 26 MapForwarder calls and Store made 10. Neither host called AddReverseProxy or…","i":"HttpResilienceDefaults.TotalRequestTimeout AddServiceDiscoveryDestinationResolver ForwarderRequestConfig.ActivityTimeout AddHttpForwarderWithServiceDiscovery ForwarderRequestConfig HttpVersion.Version20 RequestVersionExact appsettings.json AddReverseProxy IHttpForwarder LoadFromConfig ForwardHttp2"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#decision","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Decision","x":"Make configuration the single source of the gateway route table, and pin it with a test. Each gateway calls…","i":"HttpResilienceDefaults.TotalRequestTimeout Http2ForwardingConfigFilter RequestVersionExact appsettings.json MapReverseProxy IHttpForwarder RouteMapTests ForwardHttp2 IProxyConfig MapForwarder ReverseProxy HttpRequest"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#rationale","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Rationale","x":"- The drift already happened, in the repository that has the most gateway tests. ADC is the careful consumer, and it still carried three unpinned routes, an off-by-one comment…"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#trade-offs","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Trade-offs","x":"- The compiler stopped helping. A misspelled cluster reference, a malformed path pattern or a route that shadows another is a runtime failure, discovered as a 404 or a 502, where…","i":"appsettings.json ForwardHttp2 IProxyConfig MapForwarder Order"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#related","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Related","x":"ADR-008 (amended: the Gateway keeps the route-to-service map it was given, now expressed as configuration rather than as forwarder registrations), ADR-088 (the other half of this…","i":"HttpResilienceDefaults.TotalRequestTimeout"},{"u":"/docs/adr/090-event-upcaster-registration.html","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records"},{"u":"/docs/adr/090-event-upcaster-registration.html#status","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-21). Completes the follow-up named in ADR-010: that record established the versioning policy (a SchemaVersion signal plus a new-type-and-upcaster discipline for…","i":"SchemaVersion"},{"u":"/docs/adr/090-event-upcaster-registration.html#context","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Context","x":"ADR-010 splits event evolution into a signal and a discipline. The signal (SchemaVersion, a fitness-function-gated property on every integration event) shipped with ADR-010…","i":"InProcessMessageBus SchemaVersion"},{"u":"/docs/adr/090-event-upcaster-registration.html#decision","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Decision","x":"1. A typed upcaster abstraction, in the Application layer. IEventUpcaster (Source/Core/MMCA.Common.Application/Interfaces/IEventUpcaster.cs) is a pure payload mapping from a…","i":"RegisterUpcastedIntegrationEventConsumer EventUpcastersHaveUniqueSourceTypes EventUpcastersIncreaseSchemaVersion UpcastingIntegrationEventConsumer ArchitectureRules.Upcasters.cs services.AddEventUpcaster AddUserDataExportSection EventConventionTestsBase IIntegrationEventHandler IntegrationEventConsumer DomainEventDispatcher EventUpcasterRegistry"},{"u":"/docs/adr/090-event-upcaster-registration.html#rationale","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Rationale","x":"- The discipline becomes a mechanism. ADR-010's own framing (a thing that matters is a check, not a comment) now applies to the upcaster half: the transform has a first-class…","i":"SchemaVersion MessageId"},{"u":"/docs/adr/090-event-upcaster-registration.html#trade-offs","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Trade-offs","x":"- The upcast walk advances by declared target type, not runtime type. A misbehaving upcaster that returns an instance of some other type cannot send the walk into an unvalidated…","i":"UpcastingIntegrationEventConsumer OutboxMessage.DeserializeEvent OutputCacheEvictionRequested DomainEventDispatcher AddEventUpcaster"},{"u":"/docs/adr/091-cache-backed-password-reset.html","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records"},{"u":"/docs/adr/091-cache-backed-password-reset.html#status","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-22). Extends ADR-029 (the cache-backed login-protection idiom this record reuses) and ADR-032 (which decided how a password is stored, never how a user who has…"},{"u":"/docs/adr/091-cache-backed-password-reset.html#context","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Context","x":"Both consumer apps shipped authenticated password change (PUT /Auth/password) and nothing for a user who cannot sign in at all. The recorded fallback in MMCA.ADC's specification…","i":"ResetTokenExpiresAt ResetTokenHash ResetAttempts PUT"},{"u":"/docs/adr/091-cache-backed-password-reset.html#decision","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Decision","x":"1. The reset token is a cache record, not a schema change. IPasswordResetTokenService (Source/Core/MMCA.Common.Application/Auth/IPasswordResetTokenService.cs) is two methods,…","i":"CryptographicOperations.FixedTimeEquals PasswordResetAuthControllerBase IPasswordResetTokenService FindUntrackedByEmailAsync ForgotPasswordHandlerBase ResetPasswordHandlerBase PasswordReset__ResetUrl PasswordResetController ValidateAndConsumeAsync Auth.InvalidResetToken LoginProtectionService ForgotPasswordCommand"},{"u":"/docs/adr/091-cache-backed-password-reset.html#rationale","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Rationale","x":"- No migration is the whole point. A reset credential is short-lived by nature, and the expiry semantics a reset needs (a TTL, a single use, an attempt cap) are native to a cache…","i":"Result.Success"},{"u":"/docs/adr/091-cache-backed-password-reset.html#trade-offs","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Trade-offs","x":"- Cache eviction invalidates outstanding tokens. A Redis restart, an eviction under memory pressure, or a fall back to the in-memory store on a different replica all silently…","i":"IncrementAsync"},{"u":"/docs/onboarding/index.html","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","x":"A teaching guide for an experienced .NET engineer who is new to this codebase. It walks every first-party type, explaining not just what each type is but how it works and why it…","i":"CLAUDE.md dotnet new"},{"u":"/docs/onboarding/index.html#how-the-guide-is-organized-two-axes","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"How the guide is organized, two axes","x":"The guide has two organizing axes that work together. 1. Primary axis, functional grouping. Every type lives in exactly one functional group: the capability or cross-cutting…","i":"SelfHttpWarmupTask GateTestContext MMCA.Common MMCA.ADC Priority"},{"u":"/docs/onboarding/index.html#chapters","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"Chapters","x":"---","i":"AuthenticationServiceBase HttpResilienceDefaults AuthenticationService ConferencePermissions ApplicationDbContext IdentityPermissions SQLServerDbContext HealthCheckTags OutboxFinalizer HasPermission ThemeService Contracts"},{"u":"/docs/onboarding/index.html#legend-how-to-read-a-type-section","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"Legend, how to read a type section","x":"Every type gets one section using this template: {TypeName} {Assembly} · {namespace} · {file:line} · Level {n} · {kind} - What it is: one or two plain-language sentences. -…","i":"namespace Result Rubric Name"},{"u":"/docs/onboarding/index.html#suggested-reading-paths","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"Suggested reading paths","x":"- Framework-first (recommended). Primer → group-01 → upward. You meet the MMCA.Common foundations before the MMCA.ADC features that build on them; this matches dependency order…","i":"MMCA.Common MMCA.ADC Rubric"},{"u":"/docs/onboarding/index.html#the-companion-projects-context","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"The companion projects (context)","x":"This guide covers MMCA.Common (the framework) and MMCA.ADC (one consumer). MMCA.Store is out of scope. The dependency arrow is why the Common framework groups (1–16) come before…","i":"MMCA.Store"},{"u":"/docs/onboarding/00-dependency-manifest.html","d":"Phase 1: Dependency Manifest & Leveling","k":"Onboarding Guide","x":"Each distinct type node is assigned a Level by longest-path layering over its first-party dependencies (base/interface, generic constraints, field/property/param/return types,…","i":"System.Guid global static using Using int"},{"u":"/docs/onboarding/00-dependency-manifest.html#manifest-by-level-then-assembly","d":"Phase 1: Dependency Manifest & Leveling","k":"Onboarding Guide","t":"Manifest (by level, then assembly)","i":"DefaultEntityConfigurationAssemblyProviderTests GetPublicSessionCategoryItemFilterHandlerTests GetPublicSpeakerCategoryItemFilterHandlerTests AddSessionQuestionAnswerCommandValidatorTests ConferenceCategoryCreateRequestValidatorTests ConferenceCategoryUpdateRequestValidatorTests UserNotificationExportServiceGrpcAdapterTests MarkAllNotificationsReadHandlerTrackingTests SignalRPushNotificationSenderAdditionalTests UserSessionBookmarkCacheEvictionHandlerTests AddEventQuestionAnswerCommandValidatorTests AddSessionCategoryItemCommandValidatorTests"},{"u":"/docs/onboarding/00-group-taxonomy.html","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","x":"This is the primary axis of the guide. Every one of the 3,465 distinct first-party type nodes from 00-inventory.md is assigned to exactly one functional group - its primary home:…","i":"MMCA.Common MMCA.ADC Result"},{"u":"/docs/onboarding/00-group-taxonomy.html#design-notes-boundary-decisions-worth-stating-up-front","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","t":"Design notes (boundary decisions worth stating up front)","x":"- Cycles are kept whole. The 13 dependency cycles (SCCs) from the manifest are never split across groups. Notably the ApplicationDbContext AuditSaveChangesInterceptor…","i":"DomainEventSaveChangesInterceptor DataSourceModelCacheKeyFactory AuditSaveChangesInterceptor MMCA.ADC.Notification ApplicationDbContext MMCA.Common.Testing IAnonymizable PiiAttribute Gallery Rubric Fact S30"},{"u":"/docs/onboarding/00-group-taxonomy.html#the-groups-ordered","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","t":"The groups (ordered)","x":"Reconciliation: 1685 production types across 26 groups + 1780 test/testing types in G25 = 3465 (matches the inventory's distinct-node count). No type appears twice; none dropped.…"},{"u":"/docs/onboarding/00-group-taxonomy.html#group-membership","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","t":"Group membership","x":"group-01-result-error-handling.md 14 types The Result/Error railway that every operation returns instead of throwing; pagination result shapes. group-02-domain-building-blocks.md…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests SessionBookmarkValidationServiceGrpcAdapter DefaultEntityConfigurationAssemblyProvider GetPublicSessionCategoryItemFilterHandler GetPublicSpeakerCategoryItemFilterHandler SessionQuestionPendingCountChangedPayload AddSessionQuestionAnswerCommandValidator ConferenceCategoryCreateRequestValidator ConferenceCategoryUpdateRequestValidator CookieSessionRefreshMiddlewareExtensions DisabledSessionBookmarkValidationService MMCA.ADC.Conference.Infrastructure.Tests"},{"u":"/docs/onboarding/00-inventory.html","d":"Phase 0: Type Inventory","k":"Onboarding Guide","x":"Generated mechanically by a Roslyn syntactic parse of every in-scope .cs file under MMCA.Common/Source, MMCA.Common/Tests, MMCA.ADC/Source, MMCA.ADC/Tests. - Files scanned: 2810…","i":"extension"},{"u":"/docs/onboarding/00-inventory.html#full-inventory","d":"Phase 0: Type Inventory","k":"Onboarding Guide","t":"Full inventory","i":"MMCA.ADC.Conference.Application.Events.Sessionize MMCA.ADC.Conference.Application.Events.Validation MMCA.ADC.Conference.Application.Tests.Events.DTOs MMCA.ADC.Conference.Domain.Questions.DomainEvents MMCA.ADC.Conference.Infrastructure.Tests.Services MMCA.ADC.Conference.IntegrationTests.CrossService MMCA.ADC.Engagement.Application.CheckIns.Services MMCA.ADC.Engagement.Domain.LivePolls.DomainEvents MMCA.ADC.Engagement.Domain.Tests.SessionQuestions MMCA.ADC.Identity.IntegrationTests.Infrastructure MMCA.Common.Application.Interfaces.Infrastructure MMCA.Common.Application.Users.UseCases.DeleteUser"},{"u":"/docs/onboarding/00-inventory.html#extensiont-blocks","d":"Phase 0: Type Inventory","k":"Onboarding Guide","t":"extension(T) blocks","i":"IDistributedApplicationBuilder IBusRegistrationConfigurator AuthenticationBuilder IEndpointRouteBuilder WebApplicationBuilder IApplicationBuilder ICurrentUserService IReadOnlyCollection currentUserService IServiceCollection OutputCacheOptions IResourceBuilder"},{"u":"/docs/onboarding/00-inventory.html#generated--excluded-artifacts-no-type-sections-written","d":"Phase 0: Type Inventory","k":"Onboarding Guide","t":"Generated / excluded artifacts (no type sections written)","x":"118 files excluded as generated (EF migrations, snapshots, .g.cs, AssemblyInfo)."},{"u":"/docs/onboarding/00-primer.html","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","x":"This chapter teaches the cross-cutting things once, so the per-type chapters can stay focused. Read it before the group chapters (start with group-01). Everything here is either…"},{"u":"/docs/onboarding/00-primer.html#1-the-big-picture","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"1. The big picture","x":"Two codebases are in scope: - MMCA.Common: a framework, published as fifteen NuGet packages to nuget.org (the documented install path) and mirrored to GitHub Packages (ADR-053)…","i":"Testing.Architecture Aspire.Hosting Infrastructure Application MMCA.Common Testing.E2E references Testing.UI MMCA.ADC Testing UI.Maui Aspire"},{"u":"/docs/onboarding/00-primer.html#2-architectural-styles-this-codebase-commits-to","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"2. Architectural styles this codebase commits to","x":"These are the recurring ideas. Each is taught fully at its first concrete appearance in a group chapter; here is the orientation so the vocabulary is familiar. - Domain-Driven…","i":"EntityTypeConfigurationSQLServer PublicEndpointOutputCachePolicy JwtForwardingClientInterceptor FaultIntegrationEventConsumer GatewayCorrelationMiddleware JwtSettings.SigningAlgorithm EntityTypeConfigurationBase UseCommonMiddlewarePipeline TenantResolutionMiddleware ExportUserDataHandlerBase ISoftDeletedUserValidator ServiceInfoControllerBase"},{"u":"/docs/onboarding/00-primer.html#3-the-external-stack-bcl--nuget-external-level-0","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"3. The external stack (BCL / NuGet, \"external Level 0\")","x":"These are not first-party and get no per-type sections. Versions are from MMCA.Common/Directory.Packages.props and MMCA.ADC/Directory.Packages.props (Central Package Management,…","i":"Microsoft.Extensions.ServiceDiscovery.Yarp Microsoft.Extensions.Http.Resilience Notification.PushNotifications IEntityTypeConfiguration MMCA.Common.UI global.json IMessageBus SaveChanges TryDecorate DbContext OrderBy vX.Y.Z"},{"u":"/docs/onboarding/00-primer.html#4-c-build-and-code-style-conventions","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"4. C#, build, and code-style conventions","x":"- .NET 10.0, LangVersion: preview: required because the codebase uses C extension types (extension(T) syntax, see below). - Central Package Management (CPM). All NuGet versions…","i":"csharp_style_namespace_declarations MMCA.Common.Testing.Architecture ManagePackageVersionsCentrally Directory.Packages.props DependencyInjection.cs DependencyVersionTests TreatWarningsAsErrors csharp_prefer_braces EntityTypeExtensions packageSourceMapping IServiceCollection IArchitectureMap"},{"u":"/docs/onboarding/00-primer.html#5-the-solution--test-layout","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"5. The solution / test layout","x":"- .slnx: the human solution (XML format). .slnf, a solution filter used in CI to build a subset fast (MMCA.Store.CI.slnf, MMCA.ADC.CI.slnf). - Microsoft Testing Platform, not…","i":"MMCA.Common.UI.E2E.Tests MMCA.Common.UI.Gallery MMCA.Store.CI.slnf MMCA.ADC.CI.slnf csproj slnx"},{"u":"/docs/onboarding/00-primer.html#6-the-34-category-architecture-evaluation-lens","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"6. The 34-category architecture-evaluation lens","x":"This codebase is also scored against a 34-category rubric (Website/docs-src/governance/ArchitectureEvaluationCriteria.md, published at ). This guide weaves the rubric in so you…","i":"Rubric Name"},{"u":"/docs/onboarding/group-01-result-error-handling.html","d":"1. Result & Error Handling","k":"Onboarding Guide","x":"This is the first capability chapter, and it is deliberately first because the pattern it teaches underpins almost every other one in the guide. Before you read a command…","i":"ArgumentOutOfRangeException.ThrowIfNegative ArgumentNullException.ThrowIfNull DomainInvariantViolationException MMCA.Common.Shared.Serialization MMCA.Common.Shared.Abstractions System.Text.Json.Utf8JsonReader GrpcResultExceptionInterceptor System.Text.Json.Serialization MMCA.Common.Shared.Exceptions System.Buffers.Text.Base64Url Base64Url.TryDecodeFromChars ValidationFailureExtensions"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","x":"What this group covers. This is the DDD heart of the framework, the small, dependency-light primitives every business model in MMCA.Common and MMCA.ADC is built from. There are…","i":"EnumerationJsonConverterFactory AuditableAggregateRootEntity IdValueGeneratedAttribute CurrencyJsonConverter PhoneNumberInvariants EntityTypeExtensions EnumerationConverter AuditableBaseEntity MMCA.Common.Domain MMCA.Common.Shared RedactableProperty AddressInvariants"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#the-entity-chain-one-capability-per-rung","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"The entity chain, one capability per rung","x":"Read the chain bottom-up. BaseEntity (MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/BaseEntity.cs:14) is almost nothing: a single required init identifier of the per-entity…","i":"AuditableAggregateRootEntity AuditSaveChangesInterceptor ChangeTracker.Entries AuditableBaseEntity GetChildOrNotFound RemoveDomainEvents ClearDomainEvents IAuditableEntity ValidateSetItems TIdentifierType AddDomainEvent entry.Property"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#two-opt-in-markers-beside-the-chain","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Two opt-in markers beside the chain","x":"Not every cross-cutting capability belongs on the inheritance chain, because not every entity should pay for it. ITenantEntity…","i":"AuditTrailSaveChangesInterceptor TenantSaveChangesInterceptor entity.HasQueryFilter ApplyTenantFilters IAuditableEntity TenantFilterName AddMultiTenancy AuditTrailEntry IAuditedEntity AddAuditTrail configuration ITenantEntity"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#how-a-domain-event-leaves-an-aggregate","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"How a domain event leaves an aggregate","x":"The runtime flow ties this group to the events/outbox group. A command handler loads an aggregate, calls a business method, and that method calls AddDomainEvent(...); the event…","i":"DomainEventSaveChangesInterceptor context.ChangeTracker.Entries RemoveDomainEvents DomainEntityState IIntegrationEvent DeferredDispatch OutboxProcessor AddDomainEvent IAggregateRoot OutboxMessage IDomainEvent Unchanged"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#value-objects-invalid-instances-cannot-exist","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Value objects, invalid instances cannot exist","x":"The second family models concepts with no identity: two Money(10, USD) are equal because their values match, not because they are the same row. ValueObject is the cheapest…","i":"EnsurePreferredCultureIsValid EnsurePreferredThemeIsValid EnsureCollectionIsNotEmpty InvalidOperationException PhoneNumberValueConverter EnsureMoneyIsNotNegative EnsureBytesAreNotEmpty EnsureStringIsNotEmpty CurrencyJsonConverter EnsureStringMaxLength PhoneNumberInvariants EnsureIdIsNotDefault"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#smart-enumerations-a-closed-set-that-can-carry-behavior","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Smart enumerations, a closed set that can carry behavior","x":"Enumeration (MMCA.Common/Source/Core/MMCA.Common.Shared/ValueObjects/Enumeration.cs:71) is the answer to a recurring shape a CLR enum handles badly: a closed set of named members…","i":"ValueObjectsAreImmutableSealedInShared JsonSerializerOptions.Converters EnumerationJsonConverterFactory Enumeration.UnknownValue Enumeration.UnknownName CurrencyJsonConverter EnumerationConverter ReadOnlyCollection FrozenDictionary JsonConverter JsonException TEnumeration"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#governance-markers-metadata-that-other-layers-act-on","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Governance markers, metadata that other layers act on","x":"The last family is tiny attributes and helpers that carry intent the rest of the stack reads reflectively. PiiAttribute…","i":"AuditTrailSaveChangesInterceptor CultureInfo.InvariantCulture IdValueGeneratedAttribute PiiRedactor.RedactedToken EncryptedStringConverter PiiConventionTestsBase ConcurrentDictionary EntityTypeExtensions GetCustomAttribute IsIdValueGenerated MMCA.Common.Domain PiiConventionTests"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#where-this-group-sits","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Where this group sits","x":"Everything above is consumed by the layers that follow: every module entity (for example the Conference domain, Engagement, and Identity modules) derives from one of the three…","i":"EnumerationJsonConverterFactory.CreateConverter PhoneNumberInvariants.EnsurePhoneNumberIsValid AddressInvariants.EnsureAddressLine1IsValid MMCA.Common.Domain.Interfaces.IAnonymizable AddressInvariants.AddressLine1MaxLength EntityTypeExtensions.IsIdValueGenerated AddressInvariants.EnsureAddressIsValid EventInvariants.EnsureDateRangeIsValid ValueObjectsAreImmutableSealedInShared EntityTypeBuilderExtensions.OwnsMoney EntitiesWithPiiImplementAnonymizable EmailInvariants.EnsureEmailIsValid"},{"u":"/docs/onboarding/group-03-querying-specifications.html","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","x":"What this group covers. Every read in MMCA.Common and ADC (\"list the published events\", \"get session 42\", \"the speakers in Atlanta, page 3, sorted by name, with only the name and…","i":"QuerySpecification Expression IQueryable TEntity OFFSET SELECT ORDER WHERE bool Func name bio"},{"u":"/docs/onboarding/group-03-querying-specifications.html#the-specification-pattern-the-trusted-predicate","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"The Specification pattern, the trusted predicate","x":"ISpecification (MMCA.Common/Source/Core/MMCA.Common.Domain/Interfaces/ISpecification.cs:12) exposes two faces of one rule: a Criteria expression tree that EF Core translates to…","i":"PublicSessionStatusSpecification.StatusCriteria GetPublicSessionFilterHandler PublishedEventSpecification CrossSourceSpecification OwnedByUserSpecification SpecificationExtensions SpecificationComposer dependent.ForeignKey InvocationExpression Enumerable.Contains InlineSpecification s.Event.IsPublished"},{"u":"/docs/onboarding/group-03-querying-specifications.html#queryspecification-a-whole-read-in-one-object","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"QuerySpecification, a whole read in one object","x":"A plain specification is only a predicate, which leaves includes, ordering, paging, and tracking to be threaded through every layer as loose arguments. QuerySpecification…","i":"IgnoreQueryFilters QuerySpecification EFReadRepository LambdaExpression OrderExpression TIdentifierType WithSoftDeleted specification Specification BaseQueryFor IncludePaths WithTracking"},{"u":"/docs/onboarding/group-03-querying-specifications.html#dynamic-filtering-one-strategy-per-clr-type","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"Dynamic filtering, one Strategy per CLR type","x":"User filters arrive as a Dictionary , property name to operator key plus raw string value, parsed from the query string by QueryFilterModelBinder at the API edge, which caps a…","i":"Filter.Operator.NotSupported QueryParameterizationTests Filter.Property.NotFound Filter.Type.NotSupported datetimefilterstrategy QueryFilterModelBinder ResolveFilterValueType decimalfilterstrategy Filter.Value.Invalid StringFilterStrategy ResolvePropertyInfo boolfilterstrategy"},{"u":"/docs/onboarding/group-03-querying-specifications.html#sorting-sparse-fieldsets-and-paging-arithmetic","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"Sorting, sparse fieldsets, and paging arithmetic","x":"QueryFieldService (MMCA.Common/Source/Core/MMCA.Common.Application/Services/QueryFieldService.cs:16) owns the rest of read shaping. ApplySorting (QueryFieldService.cs:155)…","i":"PropertyInfo.GetValue ValidateSortDirection ApplyFieldSelection ShapeCollectionData GetShapedAccessors Expression.Lambda QueryFieldService PagingMath.Clamp PropertyAccessor MaxCacheEntries ExpandoObject ApplySorting"},{"u":"/docs/onboarding/group-03-querying-specifications.html#the-pipeline-two-entity-paths-plus-projection-pushdown","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"The pipeline: two entity paths plus projection pushdown","x":"IEntityQueryPipeline (MMCA.Common/Source/Core/MMCA.Common.Application/Services/Query/IEntityQueryPipeline.cs:10) is the execution contract, implemented by the sealed…","i":"ApplyIncludesCriteriaAndFilters inavigationmetadataprovider NavigationMetadataProvider MaxUnboundedResultLimit NavigationPropertyInfo CountUnpaginatedAsync EntityQueryParameters ExecuteProjectedAsync IEntityQueryPipeline INavigationPopulator entityquerypipeline IEntityDTOProjector"},{"u":"/docs/onboarding/group-03-querying-specifications.html#the-query-service-the-public-face","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"The query service, the public face","x":"IEntityQueryService (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/IEntityQueryService.cs:19) and its concrete EntityQueryService…","i":"SpeakerEntityQueryService BuildPaginationMetadata MaxUnboundedResultLimit TryGetByIdFastPathAsync DTOToEntityPropertyMap TryGetFastPathIncludes EntityQueryParameters ExecuteProjectedAsync PagedCollectionResult GetAllForLookupAsync INavigationPopulator DTOMapper.MapToDTOs"},{"u":"/docs/onboarding/group-03-querying-specifications.html#end-to-end-one-list-request","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"End to end, one list request","x":"The request reaches a read controller, EntityControllerBase (Group 12), which resolves MaxPageSize per request from IApplicationSettings, falling back to 500 when unset…","i":"IEntityQueryService.GetAllAsync EntityQueryParameters PagedCollectionResult EntityControllerBase IApplicationSettings EntityQueryPipeline TIdentifierType MaxPageSize PagingMath TEntityDTO requested TEntity"},{"u":"/docs/onboarding/group-03-querying-specifications.html#also-filed-here-the-best-effort-side-effect-helper","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"Also filed here: the best-effort side-effect helper","x":"Three types in this group are not part of the read path at all; they are co-located in MMCA.Common.Application/Services and are grouped by that folder. BestEffort…","i":"Filtering.DynamicQueryConfig.Parameterized IEntityQueryPipeline.ExecuteProjectedAsync MMCA.Common.Application.Services.Filtering SpecificationsDoNotNavigateToOtherEntities ArgumentException.ThrowIfNullOrWhiteSpace QueryFilterService.ResolveFilterValueType Microsoft.Extensions.DependencyInjection NavigationMetadataProvider.BuildIncludes CrossSourceSpecification.BuildCriteria MMCA.Common.Application.Services.Query MMCA.Common.Application.Specifications QueryFieldService.ApplyFieldSelection"},{"u":"/docs/onboarding/group-04-events-outbox.html","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","x":"What this chapter covers. This group is the codebase's event spine: how an aggregate says \"something happened\", how that fact is persisted so it cannot be lost, and how it…"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-two-kinds-of-event","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The two kinds of event","x":"Everything starts with two marker interfaces in the Domain layer. IDomainEvent is the base contract: a DateOccurred timestamp (when the business action happened, not when it was…","i":"BaseIntegrationEvent EntityChangedEvent DomainEntityState IIntegrationEvent BaseDomainEvent TIdentifierType Infrastructure UserRegistered SchemaVersion Architecture DateOccurred IDomainEvent"},{"u":"/docs/onboarding/group-04-events-outbox.html#raising-and-capturing-where-the-outbox-is-written","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Raising and capturing: where the outbox is written","x":"Aggregates raise events by calling AddDomainEvent() (see AuditableAggregateRootEntity in G02), which simply buffers them on the entity. Nothing is dispatched yet; the events ride…","i":"DomainEventSaveChangesInterceptor OutboxMessage.FromDomainEvent AuditableAggregateRootEntity TIdentifierType AddDomainEvent OutboxMessages OutboxMessage SavingChanges Architecture DbContext Rubric Data"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-routing-split-local-events-dispatch-in-process-integration-events-wait-for-the-bus","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The routing split: local events dispatch in-process, integration events wait for the bus","x":"Here is the detail that most people get wrong, and it is the heart of the design. After the transaction commits (SavedChanges), the interceptor does not treat all captured events…","i":"IIntegrationEventHandler IDomainEventDispatcher SafeDomainEventHandler DomainEventDispatcher someIntegrationEvent IDomainEventHandler TIntegrationEvent DbContextFactory OutboxFinalizer OutboxProcessor AddDomainEvent ExecuteUpdate"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-safety-net-how-the-processor-schedules-itself","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The safety net: how the processor schedules itself","x":"The OutboxProcessor is a BackgroundService and the most intricate type in the group; most of its complexity is about not wasting work. It exists because the steps between commit…","i":"PollingIntervalSeconds ProcessingDelaySeconds BackgroundService OutboxCycleResult ComputeWaitTime OutboxProcessor OutboxSettings ExecuteUpdate IOutboxSignal SemaphoreSlim LeaseSeconds OutboxSignal"},{"u":"/docs/onboarding/group-04-events-outbox.html#failures-dead-letters-and-keeping-the-table-and-telemetry-bounded","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Failures, dead-letters, and keeping the table (and telemetry) bounded","x":"Delivery failures split into two very different outcomes, worth keeping straight. A transient failure (a handler or broker publish throwing) increments the row's RetryCount,…","i":"OutboxPollFilterProcessor outbox.dead_letter.count DeadLetterRetentionDays RetryBackoffBaseSeconds CleanupIntervalHours OutboxCleanupService MMCA.Common.Outbox Observability OutboxMetrics RetentionDays TimeProvider Operability"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-pluggable-transport-in-process-versus-broker","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The pluggable transport: in-process versus broker","x":"Here is the boundary that makes a module extractable without rewriting its handlers. Application code that wants to publish an integration event depends on IEventBus (or on the…","i":"InProcessMessageBus AddBrokerMessaging IIntegrationEvent InProcessEventBus BrokerMessageBus OutboxFinalizer OutboxProcessor BrokerEventBus Microservices Application IMessageBus IEventBus"},{"u":"/docs/onboarding/group-04-events-outbox.html#consuming-from-the-broker-the-inbox-and-the-generic-consumer","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Consuming from the broker: the inbox and the generic consumer","x":"On the receiving side of a broker hop, application code keeps writing plain IIntegrationEventHandler implementations; there is no MassTransit-specific consumer class to author…","i":"IntegrationEventConsumerExtensions RegisterIntegrationEventConsumer IBusRegistrationConfigurator IIntegrationEventHandler IntegrationEventConsumer AlreadyProcessedAsync MarkProcessedAsync DbUpdateException NoOpInboxStore EfInboxStore InboxMessage AddConsumer"},{"u":"/docs/onboarding/group-04-events-outbox.html#putting-it-together-one-events-life","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Putting it together, one event's life","x":"To see the whole spine at once, follow a single integration event from a producer service to a consumer service in broker mode. (1) A command mutates an aggregate, which raises…","i":"MMCA.Common.Infrastructure.Persistence.Outbox MMCA.Common.Infrastructure.Persistence.Inbox Microsoft.Extensions.Hosting.IHostedService OutboxProcessor.ProcessPendingMessagesAsync UserSessionBookmarkCacheEvictionHandler services.AddOutputCacheEvictionHandler Microsoft.Extensions.Logging.ILogger MMCA.Common.Application.DomainEvents MMCA.Common.Domain.IntegrationEvents ApplicationDbContext.ConfigureInbox domainEventDispatcher.DispatchAsync MMCA.Common.Infrastructure.Services"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","x":"What this group covers. Every write and every read in an MMCA application is a use case: a small, single-purpose object (a command or a query) handed to a handler that does…","i":"AuthorizationCommandDecorator TransactionalCommandDecorator AuthorizationQueryDecorator FeatureGateCommandDecorator ValidatingCommandDecorator FeatureGateQueryDecorator ProfilingCommandDecorator CachingCommandDecorator LoggingCommandDecorator ProfilingQueryDecorator TimeoutCommandDecorator CachingQueryDecorator"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#the-shape-thin-handlers-fat-pipeline","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"The shape: thin handlers, fat pipeline","x":"A handler is deliberately tiny. ICommandHandler (MMCA.Common/Source/Core/MMCA.Common.Application/UseCases/ICommandHandler.cs:9) and IQueryHandler…","i":"cancellationToken CancellationToken ICommandHandler IQueryHandler HandleAsync Patterns TCommand default TResult Design Result Rubric"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#how-the-pipeline-is-assembled-scrutor-registration-versus-execution-order","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"How the pipeline is assembled (Scrutor, registration versus execution order)","x":"The wiring lives in DependencyInjection.cs (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:21), exposed as extension(IServiceCollection services) members…","i":"DecoratorPipelineOrderTestsBase ScanModuleApplicationServices ProfilingCommandDecorator AddApplicationDecorators DependencyInjectionTests AddApplicationProfiling ProfilingQueryDecorator DependencyInjection.cs EntityQueryPipeline IServiceCollection ServiceCollection ICommandHandler"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#why-this-exact-order-and-what-each-layer-guards","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Why this exact order, and what each layer guards","x":"The nesting order is a deliberate cost-and-correctness argument, spelled out in the registration XML-doc (DependencyInjection.cs:76-98): - Feature-gating is outermost so a…","i":"TransactionCommitAmbiguousException ICacheService.RemoveByPrefixAsync Authorization.PermissionDenied IFeatureManager.IsEnabledAsync AuthorizationCommandDecorator TransactionalCommandDecorator AuthorizationQueryDecorator FeatureGateCommandDecorator OperationCanceledException ValidatingCommandDecorator CqrsMetrics.QueryDuration ExecuteInTransactionAsync"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#opt-in-by-marker-interface-pay-only-for-what-you-use","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Opt-in by marker interface, pay only for what you use","x":"The pipeline is registered for every handler, but most decorators are dormant unless the use case asks for them. The switch is a set of tiny marker / role interfaces in…","i":"MMCA.Common.Application.UseCases GetProductByIdQuery IRequiresPermission GetTicketByIdQuery ICacheInvalidating FeatureManagement GetOrderByIdQuery GetNowNextQuery IQueryCacheable ITransactional CacheDuration IFeatureGated"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#tenant-scoping-and-the-two-lock-tables","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Tenant scoping and the two lock tables","x":"Both caching decorators are multi-tenant aware, and the reason is a nice illustration of where a cross-cutting concern has to live. ICacheService is a singleton and therefore…","i":"ICacheService.GetOrCreateAsync CachingQueryDecorator KeyedSemaphoreStripe QueryCacheKeyLocks ITenantContext TenantCacheKey CacheKeyLocks ICacheService IsResolved tenantId TenantId TResult"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#two-supporting-pieces-and-a-worked-example","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Two supporting pieces, and a worked example","x":"Two small helpers make the short-circuit decorators possible. ResultFailureFactory…","i":"cqrs.authorization.denied.count AuditableAggregateRootEntity TypeInitializationException InvalidOperationException RecordAuthorizationDenied DeleteSessionCommand DeleteSpeakerCommand ResultFailureFactory DeleteEntityCommand DeleteEntityHandler cqrs.timeout.count ICacheInvalidating"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#the-other-application-layer-contracts-in-this-group","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"The other Application-layer contracts in this group","x":"Five contracts sit beside the pipeline rather than inside it. They are declared here, in the Application layer, and implemented in Infrastructure or the composition root, which…","i":"AuditTrailSaveChangesInterceptor InProcessDistributedLock IEntityRequestMapper RedisDistributedLock ICommandWithRequest IEntityDTOProjector EntityQueryService ScheduledJobRunner cancellationToken IAuditTrailReader AuditTrailReader IAsyncDisposable"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#where-this-fits-and-the-failure-mode-contract","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Where this fits, and the failure-mode contract","x":"These contracts sit in the Application layer of Clean Architecture (primer §1), above Domain and below Infrastructure and the API. The API layer (G12) resolves a closed handler…","i":"Microsoft.FeatureManagement.IFeatureManager MMCA.Common.Application.UseCases.Decorators Microsoft.Extensions.DependencyInjection CqrsMetrics.RecordAuthorizationDenied QueryCacheKeyLocks.Locks.AcquireAsync MMCA.Common.Application.Extensions MMCA.Common.Application.Interfaces ICacheService.RemoveByPrefixAsync correlationContext.CorrelationId MMCA.Common.Application.UseCases System.Diagnostics.Metrics.Meter ConferenceCategoryCreateRequest"},{"u":"/docs/onboarding/group-06-validation.html","d":"6. Validation","k":"Onboarding Guide","x":"This chapter covers the small, framework-level validation kit that MMCA.Common.Application ships so that every consuming module validates command input the same way: a set of…","i":"AddressInvariants.AddressLine1MaxLength AddressInvariants.AddressLine2MaxLength ValidationFailureExtensions.ToErrors AddValidatorsFromAssemblyContaining AddressInvariants.CountryMaxLength AddressInvariants.ZipCodeMaxLength MMCA.Common.Application.Extensions MMCA.Common.Application.Validation System.Linq.Expressions.Expression AddressInvariants.StateMaxLength AddressInvariants.CityMaxLength MMCA.Common.Shared.ValueObjects"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html","d":"7. Persistence & EF Core","k":"Onboarding Guide","x":"What this group covers. This is the framework's data-access engine: everything between a domain aggregate and a row in a database. It is the single largest group in the guide…","i":"ApplicationDbContext SQLServerDbContext IWriteRepository SaveChangesAsync CosmosDbContext IReadRepository SqliteDbContext TIdentifierType IRepository UnitOfWork TEntity"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#one-base-context-one-class-per-engine-one-instance-per-database","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"One base context, one class per engine, one instance per database","x":"ApplicationDbContext (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/ApplicationDbContext.cs:39) is an abstract primary-constructor class over EF's…","i":"Database.CreateExecutionStrategy DataSourceModelCacheKeyFactory IX_AuditTrailEntries_Entity IX_OutboxMessages_Processed IX_InboxMessages_MessageId IX_ScheduledJobs_NextRunOn PendingModelChangesWarning IX_OutboxMessages_Pending BeginTransactionAsync ChangeTracker.Entries ApplicationDbContext EnableRetryOnFailure"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#savechanges-as-an-interceptor-pipeline","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"SaveChanges as an interceptor pipeline","x":"The base context resolves its interceptors from DI in OnConfiguring (ApplicationDbContext.cs:236-261), and registration order is execution order. The audit interceptor runs…","i":"DomainEventSaveChangesInterceptor AuditSaveChangesInterceptor DiscardAbandonedCapture IDomainEventDispatcher BeginCaptureExclusion ConditionalWeakTable EndCaptureExclusion FlushDeferredAsync GetRequiredService RemoveDomainEvents CurrentSaveUserId IIntegrationEvent"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#the-tenant-boundary-read-filter-plus-write-guard","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"The tenant boundary, read filter plus write guard","x":"Multi-tenancy (ADR-073) is two independent halves that meet in this group. The read half is the named Tenant query filter the base context applies to every non-owned…","i":"TenantSaveChangesInterceptor CrossTenantWriteException InvalidOperationException TenantDataSourceTargets TenantDataSourceTarget ApplicationDbContext IgnoreQueryFilters CurrentTenantId ITenantEntity TenantContext e.TenantId SoftDelete"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#recording-what-changed-the-audit-trail","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Recording what changed, the audit trail","x":"AuditTrailSaveChangesInterceptor (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/AuditTrail/AuditTrailSaveChangesInterceptor.cs:62) is the fourth interceptor and…","i":"AuditTrailSaveChangesInterceptor AuditTrailCleanupJob AuditTrailReader AuditTrailEntry IAuditedEntity AddAuditTrail ExecuteDelete RedactedToken RetentionDays PiiAttribute PropertyName PiiRedactor"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#repositories-and-the-unit-of-work","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Repositories and the unit of work","x":"Handlers do not touch a DbContext directly. They ask a UnitOfWork (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/UnitOfWork.cs:13) for a repository. The…","i":"TransactionCommitAmbiguousException DefaultSqlServerDbContextFactory ApplicationDbContextEFFactory DefaultCosmosDbContextFactory DefaultSqliteDbContextFactory UpdatePropertySetterBuilder EFReadRepositoryDecorator ExecuteInTransactionAsync HasPendingMigrationsAsync IPhysicalDbContextFactory ChangeTracker.HasChanges PhysicalDbContextFactory"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#routing-an-entity-to-its-database","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Routing an entity to its database","x":"The heart of ADR-006 is that every entity resolves to a DataSourceKey (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/DataSourceKey.cs:15), a (Engine,…","i":"IEntityDataSourceRegistry EntityDataSourceRegistry UseDataSourceAttribute NamespaceConventions UseDatabaseAttribute IDataSourceResolver DataSourceResolver DataSourceService DataSourceKey GetModuleName DataSources DataSource"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#two-model-finalizing-conventions","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Two model-finalizing conventions","x":"The base context adds both of its conventions in ConfigureConventions (ApplicationDbContext.cs:282-297), and each exists because a cross-cutting policy above would otherwise…","i":"CrossDataSourceDegradeConvention SoftDeleteUniqueIndexConvention IndexBuilderExtensions ConfigureConventions INavigationPopulator HasSoftDeleteFilter SoftDeleteFilterSql IndexBuilder extension IsDeleted TEntity Build"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#entity-configuration-and-engine-portability","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Entity configuration and engine portability","x":"Concrete entity configurations derive from the engine-aware EntityTypeConfiguration…","i":"DefaultEntityConfigurationAssemblyProvider IEntityConfigurationAssemblyProvider IEntityTypeConfigurationSQLServer NullableEnumerationValueConverter NullablePhoneNumberValueConverter EntityTypeConfigurationSQLServer IEntityTypeConfigurationCosmos IEntityTypeConfigurationSqlite EntityTypeConfigurationCosmos EntityTypeConfigurationSqlite PushNotificationConfiguration UserNotificationConfiguration"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#encryption-seeding-design-time-and-the-shared-helpers","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Encryption, seeding, design time, and the shared helpers","x":"A handful of supporting pieces round out the EF side. EncryptedStringConverter…","i":"PaymentReconciliationService IDesignTimeDbContextFactory DesignTimeDbContextOptions IdentityModuleDbSeederBase DesignTimeDbContextHelper NullDomainEventDispatcher PeriodicBackgroundService EncryptedStringConverter EntityDataSourceRegistry ExplicitAssemblyProvider EFQueryableExecutor DataSourceResolver"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#blobs-images-and-native-push","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Blobs, images, and native push","x":"The group also carries the storage-adjacent infrastructure services that are not EF at all, each behind an Application-layer interface with a null default so a host that has not…","i":"AzureNotificationHubNativePushSender AzureNotificationHubDeviceRegistrar AzureBlobFileStorageService ImageSharpImageProcessor NullPushDeviceRegistrar NullFileStorageService IPushDeviceRegistrar NullNativePushSender IFileStorageService ImageContentSniffer NativePushPayloads INativePushSender"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#where-this-group-sits","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Where this group sits","x":"Persistence is the concrete floor the abstract domain stands on. The entity bases and audit contracts from Group 02 are what the interceptors stamp and the query filters hide;…","i":"MMCA.Common.Application.Interfaces.Infrastructure MMCA.Common.Infrastructure.Persistence.AuditTrail MMCA.Common.Infrastructure.Persistence.DbContexts MMCA.Common.Infrastructure.Persistence.Encryption DomainEventSaveChangesInterceptor.DropDeferred EntityTypeConfiguration.ApplyEngineConventions Microsoft.Extensions.Hosting.BackgroundService AddInfrastructure_RegistersIRepositoryFactory CrossTenantWriteException.ForUnresolvedTenant ModelBuilderExtensions.ApplyAllConfigurations DangerousAcceptAnyServerCertificateValidator RelationalEventId.PendingModelChangesWarning"},{"u":"/docs/onboarding/group-08-auth.html","d":"8. Authentication & Authorization","k":"Onboarding Guide","x":"What this group covers. This is the security spine of the framework: how a caller proves who they are (authentication), how the system decides what they may do (authorization),…","i":"SessionCookieAuthenticationHandler PermissionAuthorizationHandler AuthenticationServiceBase AuthenticationValidators ClaimBasedUserIdProvider AuthorizationExtensions ILoginProtectionService IPasswordChangeableUser LoginProtectionSettings CookieSessionRefresher IAuthenticationService LoginProtectionService"},{"u":"/docs/onboarding/group-08-auth.html#tokens-one-signing-switch-two-validation-worlds","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Tokens: one signing switch, two validation worlds","x":"The framework mints two credentials on every successful login: a short-lived access token (a JWT, 15 minutes by default,…","i":"OidcDiscoveryEndpointExtensions OpenIdConnectMetadataWarmupTask GetPrincipalFromExpiredToken ExecutionAndPublication JwksEndpointExtensions RandomNumberGenerator JwtSigningAlgorithm IValidatableObject additionalClaims SigningAlgorithm PublicationOnly RsaJwksProvider"},{"u":"/docs/onboarding/group-08-auth.html#the-shared-authentication-workflow","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"The shared authentication workflow","x":"Login, registration, refresh, and revocation are not re-implemented per app. They live once in AuthenticationServiceBase…","i":"RefreshTokenRequestValidator AuthenticationServiceBase FindUntrackedByEmailAsync AuthenticationValidators OAuthCodeExchangeRequest AuthenticationResponse CancellationToken.None IAuthenticationService AuthenticationRequest AuthenticationService ChangePasswordRequest LoginRequestValidator"},{"u":"/docs/onboarding/group-08-auth.html#what-the-apps-user-aggregate-must-expose","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"What the app's User aggregate must expose","x":"The shared workflows never see an app's User class. They see four small Domain-layer contracts, each sized to one workflow, which is the [Rubric §1, SOLID] interface-segregation…","i":"GetUserPreferencesHandlerBase ChangePreferencesHandlerBase ChangePasswordHandlerBase ChangePreferencesRequest IPasswordChangeableUser UserPreferencesResponse DeleteUserHandlerBase AuditableBaseEntity RevokeRefreshToken UpdateRefreshToken UpdatePreferences IUserPreferences"},{"u":"/docs/onboarding/group-08-auth.html#passwords-and-brute-force-protection","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Passwords and brute-force protection","x":"Password material is handled by PasswordHasher (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Services/PasswordHasher.cs:12), which hashes with PBKDF2-HMAC-SHA512 at 600,000…","i":"CryptographicOperations.FixedTimeEquals ILoginProtectionService LoginProtectionSettings LoginProtectionService IDistributedCache MaxFailedAttempts MaxLockoutSeconds IPasswordHasher PasswordHasher ICacheService Email Range"},{"u":"/docs/onboarding/group-08-auth.html#reading-identity-from-claims","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Reading identity from claims","x":"Once a request is authenticated, downstream code needs the caller's identity without re-parsing the JWT. CurrentUserService…","i":"CultureInfo.InvariantCulture ClaimBasedUserIdProvider IHttpContextAccessor ICurrentUserService CurrentUserService ClaimsPrincipal IUserIdProvider AuthClaimTypes GetClaimValue Clients.User TokenService IsInRole"},{"u":"/docs/onboarding/group-08-auth.html#authorization-roles-permissions-ownership","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Authorization: roles, permissions, ownership","x":"The framework supports three overlapping authorization styles, wired together by the single AddAuthorizationPolicies() extension in AuthorizationExtensions…","i":"PermissionAuthorizationHandler AllowMissingOwnerAttribute OwnerOrAdminFilterOptions PermissionRegistryBuilder AddAuthorizationPolicies PermissionPolicyProvider AuthorizationExtensions HasPermissionAttribute AuthorizationPolicies PermissionRequirement RequireAuthenticated IPermissionRegistry"},{"u":"/docs/onboarding/group-08-auth.html#session-cookies-keeping-ssr-authenticated","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Session cookies: keeping SSR authenticated","x":"The final cluster solves a Blazor-specific problem: an interactive Blazor app keeps its access token in browser memory, but a cold server-side render (a new tab, an F5, an…","i":"CookieSessionRefreshMiddlewareExtensions SessionCookieAuthenticationExtensions SessionCookieAuthenticationHandler CookieSessionRefreshMiddleware ICookieSessionRefresher CookieSessionRefresher SessionCookieEndpoints KeyedSemaphoreStripe SessionCookieRequest SessionTokenResponse SessionTokenResult CookieTokenReader"},{"u":"/docs/onboarding/group-08-auth.html#privacy-the-data-subject-export-package","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Privacy: the data-subject export package","x":"Three members of this group belong to the privacy surface that sits beside erasure. UserDataExportDTO (MMCA.Common/Source/Core/MMCA.Common.Shared/Privacy/UserDataExportDTO.cs:15)…","i":"ExportUserDataHandlerBase DataExportControllerBase UserDataExportSectionDTO IUserDataExportSection Privacy.DataExport UserDataExportDTO PrivacyFeatures FormatVersion FeatureGate Authorize Available Subject"},{"u":"/docs/onboarding/group-08-auth.html#shared-primitives-and-adjacent-members","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Shared primitives and adjacent members","x":"Four group members are general-purpose primitives that landed in this chapter because of how the dependency grouping fell, though one of them is now load-bearing for auth.…","i":"MMCA.Common.Application.Interfaces.Infrastructure AuthorizationExtensions.AddAuthorizationPolicies context.ActionDescriptor.EndpointMetadata.OfType Microsoft.AspNetCore.Http.IHttpContextAccessor JsonWebKeyConverter.ConvertFromRSASecurityKey SessionCookieAuthenticationHandler.SchemeName Microsoft.AspNetCore.SignalR.IUserIdProvider Microsoft.IdentityModel.Tokens.JsonWebKeySet services.AddValidatorsFromAssemblyContaining ArgumentException.ThrowIfNullOrWhiteSpace CookieTokenReader.FreshAccessTokenItemKey ICookieSessionRefresher.GetOrRefreshAsync"},{"u":"/docs/onboarding/group-09-caching.html","d":"9. Caching","k":"Onboarding Guide","x":"What this group covers. Caching in this codebase is small, deliberate, and woven into the CQRS pipeline rather than scattered across handlers. The group is eight types: one port…","i":"Microsoft.Extensions.Caching.Hybrid.HybridCache HybridCacheEntryFlags.DisableUnderlyingData StackExchange.Redis.IConnectionMultiplexer Microsoft.Extensions.Options.IOptions MMCA.Common.Application.Interfaces MMCA.Common.Infrastructure.Caching DistributedCacheServiceRedisTests connectionMultiplexer.GetServers AbsoluteExpirationRelativeToNow PublicEndpointOutputCachePolicy System.Text.Json.JsonSerializer LogPrefixEvictionNoMultiplexer"},{"u":"/docs/onboarding/group-10-notifications.html","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","x":"What this group covers. This is the notification subsystem, the machinery that turns \"an organizer wants to tell every attendee something\" into messages that actually reach…","i":"INotificationRecipientProvider NullPushNotificationSender NullLiveChannelPublisher IPushNotificationSender ILiveChannelPublisher NotificationModule DevicesController INativePushSender UserNotification NotificationHub SmtpEmailSender IEmailSender"},{"u":"/docs/onboarding/group-10-notifications.html#the-layering-and-why-the-pieces-sit-where-they-do","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The layering, and why the pieces sit where they do","x":"The dependency flow of the group mirrors the framework's Clean Architecture story ([Rubric §3, Clean Architecture]). The Domain layer holds the two aggregates, PushNotification…","i":"NullNotificationRecipientProvider Notification.PushNotifications SignalRPushNotificationSender SendPushNotificationRequest SignalRLiveChannelPublisher NullPushNotificationSender PushNotificationInvariants DeviceInstallationRequest NullLiveChannelPublisher NotificationsController PushNotificationCreated PushNotificationStatus"},{"u":"/docs/onboarding/group-10-notifications.html#the-broadcast-send-flow-end-to-end","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The broadcast send flow, end to end","x":"Sending a notification is a command-side vertical slice ([Rubric §5, Vertical Slice], [Rubric §6, CQRS & Event-Driven]). An organizer POSTs to NotificationsController, which is…","i":"NotificationFeatures.PushNotifications AttendeeNotificationRecipientProvider AddNotificationApplicationServices NullNotificationRecipientProvider INotificationRecipientProvider PushNotification.NoRecipients unitOfWork.GetReadRepository SendPushNotificationCommand SendPushNotificationHandler PushNotificationDTOMapper IPushNotificationSender NotificationsController"},{"u":"/docs/onboarding/group-10-notifications.html#the-inbox-side","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The inbox side","x":"Reading and acknowledging notifications is the query/command counterpart, served by InboxController under the same feature gate and [Authorize(RequireAuthenticated)], so any user…","i":"GetUnreadNotificationCountQuery MarkAllNotificationsReadCommand MarkNotificationReadCommand MarkNotificationReadHandler ICurrentUserService.UserId GetMyNotificationsHandler UserNotification.NotFound GetMyNotificationsQuery RequireAuthenticated PushNotification UserNotification InboxController"},{"u":"/docs/onboarding/group-10-notifications.html#the-signalr-transport-and-how-it-survives-extraction","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The SignalR transport, and how it survives extraction","x":"NotificationHub is intentionally thin (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Hubs/NotificationHub.cs:16-17): it is [Authorize]d, and beyond ASP.NET's built-in…","i":"LiveChannelPublisherGrpcAdapter services__notification__grpc__0 SignalRPushNotificationSender SignalRLiveChannelPublisher NullLiveChannelPublisher LiveChannelGrpcService ILiveChannelPublisher AddPushNotifications RequireAuthorization _grpc.notification MapNotificationHub NotificationHub"},{"u":"/docs/onboarding/group-10-notifications.html#the-module-host-native-device-registration-and-the-privacy-export","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The module host, native-device registration, and the privacy export","x":"On the ADC side the whole capability is packaged by NotificationModule (MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.API/NotificationModule.cs:15), an IModule that…","i":"UserNotificationExportServiceGrpcAdapter DisabledUserNotificationExportService UserNotificationExportGrpcService IUserNotificationExportService UserNotificationExportItemDTO UserNotificationExportService currentUserService.UserId DeviceInstallationRequest AddNotificationModule IPushDeviceRegistrar RequiresDependencies DependencyInjection"},{"u":"/docs/onboarding/group-10-notifications.html#where-this-group-sits","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"Where this group sits","x":"Upstream, this group depends on the domain building blocks of Group 02 (both aggregates derive from AuditableAggregateRootEntity ), the Result pattern of Group 01, the CQRS…","i":"LiveChannelPushService.LiveChannelPushServiceBase MMCA.Common.Application.Interfaces.Infrastructure MMCA.ADC.Notification.Shared.UserNotifications attendeeQueryService.GetAttendeeUserIdsAsync services.AddNotificationApplicationServices PushNotificationDTOProjection.ProjectToDTO PushNotificationProjectionTranslationTests MMCA.Common.API.Controllers.Notifications NotificationHub.ReceiveNotificationMethod PushNotificationInvariants.TitleMaxLength Microsoft.Extensions.DependencyInjection PushNotificationInvariants.BodyMaxLength"},{"u":"/docs/onboarding/group-11-navigation-populators.html","d":"11. Navigation Metadata & Populators (EF-decoupled eager loading)","k":"Onboarding Guide","x":"EF Core gives you .Include() for eager loading, and for a single SQL Server database that is the right tool. But this codebase is a database-per-service modular monolith…","i":"navigationMetadata.UnsupportedIncludes.Count MMCA.Common.Application.Services.Navigation NavigationLoader.LoadChildrenPropertyAsync NavigationMetadataProvider.BuildIncludes NavigationMetadata.UnsupportedIncludes IDataSourceService.HaveIncludeSupport NavigationLoader.LoadFKPropertyAsync INavigationPopulator.PopulateAsync MMCA.Common.Application.Interfaces DeclarativeNavigationPopulator.cs CrossDataSourceDegradeConvention EntityQueryPipeline.ExecuteAsync"},{"u":"/docs/onboarding/group-12-api-hosting-mapping.html","d":"12. API Hosting, Middleware, Idempotency & DTO/Contract Mapping","k":"Onboarding Guide","x":"What this group covers. This is the ASP.NET Core edge of the framework: the layer that turns an HTTP request into a domain call and turns a Result back into an HTTP response.…","i":"Microsoft.Extensions.Localization.LocalizedString Microsoft.AspNetCore.Http.IProblemDetailsService Microsoft.EntityFrameworkCore.DbUpdateException Microsoft.AspNetCore.Http.IHttpContextAccessor Microsoft.IdentityModel.Tokens.JsonWebKeySet System.Threading.RateLimiting.RateLimitLease IDbContextFactory.HasPendingMigrationsAsync AuthorizationPolicies.RequireAuthenticated Microsoft.AspNetCore.Authentication.Google StackExchange.Redis.IConnectionMultiplexer ArgumentException.ThrowIfNullOrWhiteSpace System.Threading.RateLimiting.RateLimiter"},{"u":"/docs/onboarding/group-13-grpc-contracts.html","d":"13. gRPC & Inter-Service Contracts","k":"Onboarding Guide","x":"What this chapter is about. Once the ADC modules stopped sharing a process and became four separate service hosts (Identity, Conference, Engagement, Notification), the in-process…","i":"Microsoft.AspNetCore.Http.IHttpContextAccessor ArgumentException.ThrowIfNullOrWhiteSpace Microsoft.Extensions.DependencyInjection ErrorHttpMapping.ErrorTypeToStatusCode AddConferenceSessionValidationClient Microsoft.Extensions.Http.Resilience Microsoft.Extensions.Logging.ILogger ResultGrpcExtensions.ThrowIfFailure ResultGrpcExtensions.ToRpcException Grpc.Core.Interceptors.Interceptor ArgumentNullException.ThrowIfNull ISessionBookmarkValidationService"},{"u":"/docs/onboarding/group-14-module-system-composition.html","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","x":"What this chapter covers. This is the wiring layer, the code that turns a pile of layered assemblies into a running host. It answers three questions a new host author asks: how…","i":"ConnectionStringSettings InProcessDistributedLock PushNotificationSettings UseDataSourceAttribute RedisDistributedLock UseDatabaseAttribute ApplicationSettings DataSourcesSettings DependencyInjection FileStorageSettings PersistenceSettings AuditTrailSettings"},{"u":"/docs/onboarding/group-14-module-system-composition.html#the-module-contract-and-the-boundary-it-creates","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"The module contract and the boundary it creates","x":"A module is the unit of cohesion above a feature slice: Conference, Engagement, Identity, Notification. Each one implements IModule…","i":"DisabledSessionBookmarkValidationService DisabledEventLiveValidationService GetSessionBookmarkCountHandler IBookmarkCountService RegisterDisabledStubs RequiresDependencies AddConferenceModule applicationSettings ConferenceModule moduleEnabled Dependencies Register"},{"u":"/docs/onboarding/group-14-module-system-composition.html#discovery-and-kahn-ordered-registration","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Discovery and Kahn-ordered registration","x":"ModuleLoader (MMCA.Common/Source/Core/MMCA.Common.Application/Modules/ModuleLoader.cs:15) is the engine. Its DiscoverAndRegister comes in two overloads: the short one…","i":"AppDomain.CurrentDomain.GetAssemblies ModulesSettings.IsModuleEnabled ValidateModuleDependencies ValidateRemoteDependencies Activator.CreateInstance IModuleSeeder.SeedAsync RegisterDisabledStubs RegisterEnabledModule RequiresDependencies DisabledModuleNames DiscoverAndRegister RemoteDependencies"},{"u":"/docs/onboarding/group-14-module-system-composition.html#the-two-composition-roots-and-the-ordering-they-enforce","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"The two composition roots and the ordering they enforce","x":"Service registration itself lives in two static DependencyInjection classes, each using a C extension(IServiceCollection services) block (see primer §4 for the extension(T)…","i":"ScanModuleApplicationServices FeatureGateCommandDecorator INavigationMetadataProvider AddNativePushNotifications AddApplicationDecorators InProcessDistributedLock AddApplicationProfiling AddAzureBlobFileStorage CommandRequestValidator LoggingCommandDecorator IConnectionMultiplexer IDomainEventDispatcher"},{"u":"/docs/onboarding/group-14-module-system-composition.html#opt-in-platform-features-are-composed-the-same-way","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Opt-in platform features are composed the same way","x":"Four newer capabilities are registered beside the roots rather than inside them, and they share one discipline: registering a feature is not the same as turning it on.…","i":"AuditTrailSaveChangesInterceptor TenantSaveChangesInterceptor AddUserDataExportSection TenancySettingsValidator IUserDataExportSection MMCA.Common.Scheduler AuditTrailEntryDTO AuditTrailSettings ScheduledJobRunner AddInfrastructure BackgroundService ScheduledJobEntry"},{"u":"/docs/onboarding/group-14-module-system-composition.html#assembly-anchors","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Assembly anchors","x":"Several pieces of machinery need a Type whose Assembly identifies a layer: Scrutor's FromAssemblyOf () scans, FluentValidation's AddValidatorsFromAssemblyContaining (), and…","i":"AddValidatorsFromAssemblyContaining AddInfrastructure AssemblyReference AddApplication ClassReference FromAssemblyOf AssemblyName Assembly static class Type"},{"u":"/docs/onboarding/group-14-module-system-composition.html#configuration-binding-the-settings-family","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Configuration binding, the Settings family","x":"Everything a host operator tunes arrives as a strongly-typed settings object bound from an appsettings.json section, each carrying a static readonly string SectionName so the…","i":"TenantDataSourceOverrideSettings EffectiveExcludedPathPrefixes ScheduledJobOverrideSettings IValidatableObject.Validate IConnectionStringSettings IPushNotificationSettings SQLServerConnectionString ConnectionStringSettings EffectiveResolutionOrder PushNotificationSettings TenancySettingsValidator TenantResolutionStrategy"},{"u":"/docs/onboarding/group-14-module-system-composition.html#the-two-routing-attributes","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"The two routing attributes","x":"Two attributes, both in MMCA.Common.Infrastructure, both Inherited = true so they ride down a configuration class hierarchy, encode where an entity is stored declaratively: the…","i":"MMCA.Common.Infrastructure EntityDataSourceRegistry UseDataSourceAttribute UseDatabaseAttribute DataSourceResolver DbContextFactory DataSource Inherited Domain true"},{"u":"/docs/onboarding/group-14-module-system-composition.html#shared-user-use-case-bases-composition-in-the-other-direction","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Shared user use-case bases: composition in the other direction","x":"The chapter's last family is composition at the handler level rather than the container level. ADC and Store each own an Identity module, and five of their account use cases had…","i":"UserOwnershipRule.CheckOwnership GetUserPreferencesHandlerBase UserDataExportSectionDefaults ChangePreferencesHandlerBase UserDataExportSectionResult ChangePasswordHandlerBase ExportUserDataHandlerBase ISoftDeletedUserValidator SoftDeletedUserValidator GetUserPreferencesQuery IUserDataExportSection DeleteUserHandlerBase"},{"u":"/docs/onboarding/group-14-module-system-composition.html#end-to-end-one-hosts-boot","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"End-to-end: one host's boot","x":"Reading MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs top to bottom shows the whole chapter cooperating. The host binds and validates ApplicationSettings and…","i":"MMCA.Common.Application.Interfaces.Infrastructure MMCA.Common.Application.Users.UseCases.DeleteUser UserDataExportSectionDefaults.UnavailableReason TenancySettingsValidator.ConnectionStringFor DefaultEntityConfigurationAssemblyProvider ArgumentException.ThrowIfNullOrWhiteSpace InProcessDistributedLock.TryAcquireAsync Microsoft.Extensions.DependencyInjection ScheduledJobRunner.ResolveCronExpression UserDataExportSectionResult.Unavailable UserUseCaseLog.ExportSectionUnavailable DbContextFactory.ResolveTenantOverride"},{"u":"/docs/onboarding/group-15-common-ui-framework.html","d":"15. Common UI Framework (MudBlazor components, theme, base pages)","k":"Onboarding Guide","x":"What this chapter covers. MMCA.Common.UI is the Blazor presentation package, and it is one of the two layers (with Grpc) allowed to reference Shared only (see primer §1). It…","i":"Microsoft.AspNetCore.Components.NavigationManager Microsoft.Extensions.Configuration.IConfiguration Microsoft.AspNetCore.Builder.IApplicationBuilder Microsoft.AspNetCore.Components.DynamicComponent MauiBackNavigationBridge.HandleBackPressedAsync Microsoft.AspNetCore.WebUtilities.QueryHelpers WasmTokenStorageService.GetAccessTokenAsync HttpResilienceDefaults.TotalRequestTimeout ArgumentException.ThrowIfNullOrWhiteSpace CultureInfo.DefaultThreadCurrentUICulture ITokenStorageService.GetAccessTokenAsync Microsoft.Extensions.DependencyInjection"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","x":"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…","i":"MMCA.Common.Aspire.Gateway MMCA.Common.Aspire.Hosting AddServiceDefaults MMCA.Common.Aspire MMCA.Common.Shared MMCA.ADC.AppHost Aspire.Hosting dotnet run"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#the-orchestrator-declaring-the-resource-graph","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"The orchestrator: declaring the resource graph","x":"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.…","i":"LoginProtection__MaxRegistrationsPerIpPerHour ConnectionStrings__SQLServerConnectionString Authentication__JwtBearer__Authority WithE2eRegistrationThrottleLift E2E_LIFT_REGISTRATION_THROTTLE E2eRegistrationsPerIpPerHour __SQLServerConnectionString MMCA.Common.Aspire.Hosting DefaultBrokerResourceName E2E_JWT_PRIVATE_KEY_PEM WithSQLServerDataSource E2E_JWT_PUBLIC_KEY_PEM"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#startup-ordering-and-the-grpc-deadlock-avoidance-trick","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Startup ordering and the gRPC deadlock-avoidance trick","x":"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,…","i":"ISessionBookmarkValidationService IBookmarkCountService AddTypedGrpcClient WaitFor"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#the-service-baseline-addservicedefaults","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"The service baseline: AddServiceDefaults()","x":"Every running host calls one method first in Program.cs: AddServiceDefaults() from the single framework-grade Extensions in MMCA.Common.Aspire (Level 2,…","i":"EnableMultipleHttp2Connections ConfigureHttpClientDefaults AddDefaultHealthChecks ConfigureOpenTelemetry AddServiceDiscovery AddServiceDefaults AddWarmupReadiness MMCA.Common.Aspire SocketsHttpHandler HttpClient Program.cs TBuilder"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#one-source-of-truth-for-resilience-numbers","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"One source of truth for resilience numbers","x":"The numbers that pipeline uses are not literals in the Aspire package. They live in HttpResilienceDefaults (Level 0,…","i":"MMCA.Common.Infrastructure MMCA.Common.Grpc Continuity properties Resilience RetryCount including Business Concerns lifetime sampling attempt"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#listeners-and-probes-one-kestrel-profile-per-host-shape","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Listeners and probes: one Kestrel profile per host shape","x":"Before a service can answer a health probe it has to be listening on a protocol the prober speaks, and that is not free when the same port serves inbound gRPC.…","i":"redeclareCleartextEndpoint ASPNETCORE_HTTP_PORTS HttpProtocols.Http2 MapDefaultEndpoints BuildListenerPlan HTTP_1_1_REQUIRED Http1AndHttp2 Deployment Protocols deployed profiles httpGet"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#health-checks-liveness-readiness-and-the-optional-tag","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Health checks: liveness, readiness, and the \"optional\" tag","x":"MapDefaultEndpoints() (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:336) exposes the three-probe surface the platform reads: /health (every check, for humans and…","i":"AddInfrastructureHealthChecks AddDefaultHealthChecks MapDefaultEndpoints requireSqlServer Observability Operability Deployment optional Optional DevOps Rubric Ready"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#telemetry-what-gets-exported-and-what-it-costs","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Telemetry: what gets exported, and what it costs","x":"ConfigureOpenTelemetry() (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:121) wires logging with formatted messages and scopes (:123-127), metrics, and tracing. It…","i":"APPLICATIONINSIGHTS_CONNECTION_STRING ActivityTraceFlags.Recorded OTEL_EXPORTER_OTLP_ENDPOINT AddOpenTelemetryExporters IsInstrumentationDisabled TraceIdRatioBasedSampler MMCA.Common.Idempotency MMCA.Common.OutputCache ConfigureOpenTelemetry MMCA.Common.BestEffort TryGetTraceSampleRatio MMCA.Common.Scheduler"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#warm-up-defeating-aca-cold-start","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Warm-up: defeating ACA cold-start","x":"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…","i":"RequireSuccessStatusCode HealthCheckTags.Ready WebApplicationFactory Interlocked.Exchange RequestVersionPolicy AddServiceDefaults AddWarmupReadiness ApplicationStarted IHttpClientFactory BackgroundService ResolveWarmupPort WithJwksDiscovery"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#configuration-secrets-the-vault-as-one-more-configuration-source","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Configuration secrets: the vault as one more configuration source","x":"Connection strings, RSA signing keys and OAuth client secrets have to reach the process somehow, and KeyVaultConfigurationExtensions (Level 0,…","i":"AddCommonDataProtection DefaultAzureCredential builder.Configuration ConfigurationManager AddServiceDefaults IConfiguration Deployment Security answer DevOps Rubric the"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#security-headers-cors-and-the-shared-key-ring-at-the-host-edge","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Security headers, CORS, and the shared key ring at the host edge","x":"The next boundary in this group hardens the request and response edge. The shared response-header middleware is defined entirely in…","i":"AddCommonSecurityHeaders UseCommonSecurityHeaders AddCommonDataProtection DefaultAzureCredential AddCommonGatewayCors AddCommonBlazorCsp PermissionsPolicy MMCA.Common.API TryAddSingleton ReferrerPolicy AddCommonCors FrameOptions"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#the-gateway-edge-kit-correlation-rate-limiting-downstream-readiness","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"The gateway edge kit: correlation, rate limiting, downstream readiness","x":"A YARP gateway is the one process every client request passes through, and it is also the one host that has no application container: no DbContext, no module loader, no…","i":"AddServiceDiscoveryDestinationResolver AddGatewayDownstreamHealthChecks Connection.RemoteIpAddress MMCA.Common.Aspire.Gateway Validator.ValidateObject HttpResponse.OnStarting ValidateDataAnnotations AddGatewayRateLimiting GlobalConcurrencyLimit UseGatewayRateLimiting ConfigureClusterAsync RequestVersionOrLower"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#how-it-all-fits-at-runtime","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"How it all fits at runtime","x":"Putting the pieces in sequence: the AppHost declares the graph and injects per-service env vars (WithSQLServerDataSource, WithBroker, WithJwksDiscovery, the E2E helpers, and the…","i":"Azure.Extensions.AspNetCore.Configuration.Secrets Azure.Extensions.AspNetCore.DataProtection.Blobs identityService.WithE2eRegistrationThrottleLift AddGatewayDownstreamHealthChecks_IsIdempotent LoginProtection__MaxRegistrationsPerIpPerHour ConnectionStrings__SQLServerConnectionString database.Resource.ConnectionStringExpression Readiness_IncludesADownstreamCheckPerService ResilienceCircuitBreakerFaultInjectionTests WarmupReadinessHealthCheck.CheckHealthAsync HttpKeepAlivePingPolicy.WithActiveRequests cancellationToken.IsCancellationRequested"},{"u":"/docs/onboarding/group-17-conference-domain.html","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","x":"What this chapter covers. This is the heart of the Atlanta Developers Conference application, the Conference bounded context, the largest and richest domain in MMCA.ADC. It…","i":"AuditableAggregateRootEntity MMCA.ADC.Conference.Shared IdValueGeneratedAttribute INavigationPopulator EntityChangedEvent DomainEntityState TIdentifierType IAuditedEntity IModule TEntity Design Result"},{"u":"/docs/onboarding/group-17-conference-domain.html#two-packages-one-bounded-context","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Two packages, one bounded context","x":"The Conference context spans two of the module's projects, and the split is deliberate Clean Architecture ([Rubric §3, Clean Architecture]). MMCA.ADC.Conference.Domain holds the…","i":"ISessionBookmarkValidationService IEventLiveValidationService MMCA.ADC.Conference.Domain MMCA.ADC.Conference.Shared SessionFeedbackSubmitted SpeakerUnlinkedFromUser EventFeedbackSubmitted SpeakerLinkedToUser MMCA.Common.Domain AssemblyReference ClassReference Architecture"},{"u":"/docs/onboarding/group-17-conference-domain.html#seven-aggregates-and-their-ownership-boundaries","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Seven aggregates and their ownership boundaries","x":"An aggregate is a root entity plus the children it exclusively owns; invariants are enforced inside the boundary, and references across aggregates are by ID, never by object…","i":"AuditableAggregateRootEntity RecordSessionizeRefresh SessionQuestionAnswer SpeakerQuestionAnswer EventQuestionAnswer SessionCategoryItem SpeakerCategoryItem IDENTITY_INSERT TIdentifierType IAuditedEntity QuestionEntity QuestionSource"},{"u":"/docs/onboarding/group-17-conference-domain.html#the-aggregate-shape-taught-once","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"The aggregate shape, taught once","x":"Open any of the roots and you will see the same skeleton; this repetition is the point, and it is what makes the per-type sections that follow read quickly. The shape, using…","i":"Session.SessionQuestionAnswers Event.EventQuestionAnswers IReadOnlyCollection RestoreEventSpeaker isIdValueGenerated _rooms.AsReadOnly Result.Combine Architecture IsCollection base.Delete Performance RestoreRoom"},{"u":"/docs/onboarding/group-17-conference-domain.html#invariants-business-rules-as-testable-units","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Invariants, business rules as testable units","x":"Each aggregate has a co-located static invariant class, EventInvariants (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Domain/Events/EventInvariants.cs:10),…","i":"System.Net.Mail.MailAddress CategoryInvariants QuestionInvariants SessionInvariants SpeakerInvariants SponsorInvariants CommonInvariants EventInvariants SessionStatuses Result.Combine Decline_Queue Accept_Queue"},{"u":"/docs/onboarding/group-17-conference-domain.html#domain-events-and-the-outbox-spine","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Domain events and the outbox spine","x":"Every state-changing method raises a domain event through the inherited AddDomainEvent(...). The events come in two shapes. The aggregate-level ones, EventChanged,…","i":"SessionCategoryItemChanged SpeakerCategoryItemChanged SessionSpeakerChanged PreviousLinkedUserId CategoryItemChanged EventSpeakerChanged EntityChangedEvent DomainEntityState SaveChangesAsync CategoryChanged QuestionChanged TIdentifierType"},{"u":"/docs/onboarding/group-17-conference-domain.html#the-cross-aggregate-cascade-a-pure-domain-service","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"The cross-aggregate cascade: a pure domain service","x":"One business rule cannot live inside a single aggregate: deleting an Event must also delete every Session belonging to it (BR-127) and every Sponsor sold against it, but sessions…","i":"IEventCascadeDeletionDomainService EventCascadeDeletionDomainService EventId Session Sponsor Design Rubric Event List"},{"u":"/docs/onboarding/group-17-conference-domain.html#read-models-and-the-ai-decision-support-feature","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Read models and the AI decision-support feature","x":"The largest cluster in Conference.Shared is the DTO layer, the wire contracts that decouple the API from the domain entities ([Rubric §9, API & Contract Design]; ADR-001 chose…","i":"RefreshFromSessionizeResultDTO RefreshFromSessionizeCommand SessionSelectionDashboardDTO ScoreEventSessionsResultDTO CategoryGroupDistribution Conference.Infrastructure CategoryItemDistribution SessionQuestionAnswerDTO SpeakerQuestionAnswerDTO SpeakerSessionOverlapDTO CategoryDistributionDTO ConcurrencyTokenRequest"},{"u":"/docs/onboarding/group-17-conference-domain.html#authorization-vocabulary-and-current-event-selection","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Authorization vocabulary and current-event selection","x":"Two more Shared helpers deserve a mention because they encode policy the whole module relies on. ConferencePermissions…","i":"TimeZoneInfo.ConvertTimeToUtc ConferenceReadAudience ConferencePermissions CurrentEventDefaults CurrentEventSelector ContentManagement ContentEditor HasPermission Organizer RoleNames StartDate EventDTO"},{"u":"/docs/onboarding/group-17-conference-domain.html#crossing-the-module-boundary-contracts-stubs-and-integration-events","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Crossing the module boundary: contracts, stubs, and integration events","x":"Conference does not live alone. Three kinds of connection point join it to other modules, and all live in Conference.Shared so neither side reaches into the other's domain…","i":"DisabledSessionBookmarkValidationService DisabledEventLiveValidationService ISessionBookmarkValidationService IEventLiveValidationService QuestionModerationDefault SessionFeedbackSubmitted SpeakerUnlinkedFromUser Conference.Application EventFeedbackSubmitted BaseIntegrationEvent User.LinkedSpeakerId SpeakerLinkedToUser"},{"u":"/docs/onboarding/group-17-conference-domain.html#end-to-end-one-organizer-action","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"End-to-end: one organizer action","x":"To see the chapter cooperate, follow an organizer renaming a room on an event. The application handler loads the Event aggregate (with its Rooms hydrated by the navigation…","i":"CategoryInvariants.EnsureCategoryItemNameIsUnique IEventLiveValidationService.GetEventLiveInfoAsync MMCA.ADC.Conference.Domain.Questions.DomainEvents MMCA.ADC.Conference.Domain.Sessions.DomainEvents MMCA.ADC.Conference.Domain.Speakers.DomainEvents MMCA.ADC.Conference.Domain.Sponsors.DomainEvents IUserSpeakerLinkService.ClearLinkedSpeakerAsync MMCA.ADC.Conference.Domain.Events.DomainEvents SessionInvariants.EnsureAnswerValueIsValid SpeakerInvariants.EnsureAnswerValueIsValid CurrentEventSelector.SelectCurrentOrNext DisabledSessionBookmarkValidationService"},{"u":"/docs/onboarding/group-18-conference-application.html","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","x":"What this chapter covers. This is the application layer of the Conference module, the largest single application assembly in the codebase (this group covers 251 distinct types).…","i":"MMCA.Common.Application ics"},{"u":"/docs/onboarding/group-18-conference-application.html#the-vertical-slice-anatomy-of-a-use-case","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"The vertical-slice anatomy of a use case","x":"Open any feature folder under Sessions/UseCases/, Events/UseCases/, Speakers/UseCases/, Sponsors/UseCases/, Categories/UseCases/, or Questions/UseCases/ and you will find the…","i":"ValidateRoomAssignmentAsync BuildOverlapPredicate EventQuestionAnswers UnprocessableEntity EventSpeakers int.MinValue HandleAsync s.StartsAt DbContext s.EndsAt startsAt Session"},{"u":"/docs/onboarding/group-18-conference-application.html#manual-mapping-validation-rule-fragments-and-authorization-specifications","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Manual mapping, validation rule fragments, and authorization specifications","x":"Three sibling families recur across every aggregate. DTO mappers (SessionDTOMapper, EventDTOMapper, SpeakerDTOMapper, SponsorDTOMapper, RoomDTOMapper, CategoryItemDTOMapper, and…","i":"TimeZoneInfo.FindSystemTimeZoneById s.Event.IsPublished AbstractValidator GetProjectedAsync GetReadRepository Session.EventId SessionSpeaker e.IsPublished EventSpeaker Expression IsEligible StartDate"},{"u":"/docs/onboarding/group-18-conference-application.html#query-services-navigation-populators-and-the-composition-root","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Query services, navigation populators, and the composition root","x":"Read paths do not get bespoke handlers for the common cases; they go through the framework's generic IEntityQueryService , which supplies filtering, sorting, paging, and field…","i":"ScanModuleApplicationServices IServiceCollection ClassReference extension FirstName FullName LastName Question Sponsor"},{"u":"/docs/onboarding/group-18-conference-application.html#event-driven-reactions-domain-and-integration-handlers","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Event-driven reactions: domain and integration handlers","x":"The application layer is also where the module reacts to events. Domain event handlers implement IDomainEventHandler and run in-process after the aggregate's SaveChangesAsync.…","i":"EnsureNotServiceSession SpeakerUnlinkedFromUser EnsureStatusIsEligible User.LinkedSpeakerId SpeakerLinkedToUser GetLiveWindowUtc SaveChangesAsync SessionChanged UserRegistered LogAndRethrow IEventBus Deleted"},{"u":"/docs/onboarding/group-18-conference-application.html#attendee-facing-read-models-calendar-export-and-nownext","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Attendee-facing read models: calendar export and Now/Next","x":"A small cluster of queries serves the public schedule surfaces without going through the generic query service, because their output is not a DTO list. ExportEventCalendarHandler…","i":"CalendarExportMapper.IsExportable DateTimeOffset.UtcNow GetNowNextHandler GetLiveWindowUtc Error.NotFound IsExportable TimeProvider DTSTAMP Result string ics"},{"u":"/docs/onboarding/group-18-conference-application.html#the-sessionize-import-strategy-pattern-orchestration","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"The Sessionize import: Strategy-pattern orchestration","x":"The single most involved use case is importing a conference agenda from Sessionize.com. Sessionize returns one JSON payload covering five interdependent entity families…","i":"ThrowIfCancellationRequested TimeoutRejectedException BrokenCircuitException NotSupportedException RequestIdentityInsert HttpRequestException SaveChangesAsync JsonException TimeProvider Create Update catch"},{"u":"/docs/onboarding/group-18-conference-application.html#decision-support-ai-scoring-and-content-analytics","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Decision support: AI scoring and content analytics","x":"The last cluster is session-selection decision support, analytics that help organizers triage proposals. GetSessionSelectionDashboardHandler is a composite query: it validates…","i":"MMCA.ADC.Conference.Application.Events.Sessionize MMCA.ADC.Conference.Application.Events.Validation SessionRoomScheduling.ValidateRoomAssignmentAsync currentUserService.IsPrivilegedConferenceReader eventCascadeDeletionDomainService.CascadeDelete IUserSpeakerLinkService.ClearLinkedSpeakerAsync MMCA.ADC.Conference.Application.Categories.DTOs MMCA.ADC.Engagement.Shared.UserSessionBookmarks PublicSessionStatusSpecification.StatusCriteria SessionSimilarityCalculator.CalculateSimilarity cancellationToken.ThrowIfCancellationRequested EventInvariants.OrganizerContactEmailMaxLength"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","x":"What this chapter covers. This is the adapter layer of the Conference module, the place where the engine-agnostic domain meets concrete technology. Three concerns live here: (1)…","i":"SessionScoringQueue ISessionizeService IAiScoringService Architecture DbContext Rubric Clean DbSet"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#engine-agnostic-entities-engine-chosen-by-the-config-base-class","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Engine-agnostic entities, engine chosen by the config base class","x":"The most important idea in this chapter is one the entities themselves never express: what storage engine each entity uses is decided here, not in the domain. A Conference domain…","i":"EntityTypeConfigurationSQLServer EntityDataSourceRegistry EntityTypeConfiguration DataSource.SQLServer SessionConfiguration SpeakerConfiguration SponsorConfiguration EventConfiguration TIdentifierType UseDataSource Session Speaker"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#each-config-inherits-the-cross-cutting-behavior-then-adds-entity-specifics","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Each config inherits the cross-cutting behavior, then adds entity specifics","x":"Every configuration's Configure method begins with base.Configure(builder) (for example SessionConfiguration.cs:18) and then adds its own mappings. That one base call is where…","i":"SessionQuestionAnswerConfiguration SpeakerQuestionAnswerConfiguration EventQuestionAnswerConfiguration SessionCategoryItemConfiguration SessionInvariants.TitleMaxLength SpeakerCategoryItemConfiguration ConferenceCategoryConfiguration SoftDeleteUniqueIndexConvention SponsorInvariants.NameMaxLength EventInvariants.NameMaxLength EventQuestionAnswer.EventId NullableEmailValueConverter"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#dbsets-the-context-shape-and-how-the-configurations-are-actually-found","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"DbSets, the context shape, and how the configurations are actually found","x":"ModuleApplicationDbContext (MMCA.ADC.Conference.Infrastructure/Persistence/DbContexts/ModuleApplicationDbContext.cs:19) is the Conference module's abstract DbContext. It does one…","i":"IEntityTypeConfigurationSQLServer MMCA.ADC.Conference.Service ModuleApplicationDbContext SessionQuestionAnswers SpeakerQuestionAnswer ApplicationDbContext EventQuestionAnswers SessionCategoryItems SpeakerCategoryItems dbo.OutboxMessages SQLServerDbContext SaveChangesAsync"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#seeding-two-real-events-always-sample-data-only-in-dev-and-ci","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Seeding: two real events always, sample data only in dev and CI","x":"ConferenceModuleDbSeeder (MMCA.ADC.Conference.Infrastructure/Persistence/DbContexts/Seeding/ConferenceModuleDbSeeder.cs:24) derives from the framework's DbSeeder and runs after…","i":"ConferenceModuleDbSeeder ConferenceModuleSeeder ManualIdRangeStart QuestionInvariants includeSampleData SessionInvariants ExistsAsync DbSeeder sf1nopko z1ecmzux Migrate"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#the-sessionize-adapter","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"The Sessionize adapter","x":"SessionizeService (MMCA.ADC.Conference.Infrastructure/Services/SessionizeService.cs:10) is a deliberately thin HTTP client: the whole class is one method. Given a Sessionize…","i":"EnsureSuccessStatusCode DependencyInjection SessionizeResponse SessionizeService HttpClient GetAsync code"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#the-anthropic-ai-scoring-adapter","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"The Anthropic AI scoring adapter","x":"AnthropicScoringService (MMCA.ADC.Conference.Infrastructure/Services/AnthropicScoringService.cs:16) is the richer of the two adapters: it scores one session proposal against a…","i":"CultureInfo.InvariantCulture OperationCanceledException AnthropicScoringService AnthropicContentBlock SessionScoringResult AnthropicResponse IAiScoringService AnthropicMessage AnthropicRequest JsonPropertyName AiScoreResponse LoggerMessage"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#scoring-runs-on-a-hosted-drain-guarded-across-replicas","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Scoring runs on a hosted drain, guarded across replicas","x":"SessionScoringProcessor (MMCA.ADC.Conference.Infrastructure/Services/SessionScoringProcessor.cs:49) is the piece that makes a multi-minute paid AI pass safe to trigger from an…","i":"MMCA.ADC.Conference.Scoring scoring.run.failed.terminal ScoreEventSessionsCommand SessionScoringProcessor queue.MarkCompleted SessionScoringQueue BackgroundService CreateAsyncScope IDistributedLock TryAcquireAsync conferenceApp MarkCompleted"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#di-wiring-and-a-deliberate-resilience-override","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"DI wiring and a deliberate resilience override","x":"DependencyInjection (MMCA.ADC.Conference.Infrastructure/DependencyInjection.cs:11) is a single extension(IServiceCollection) block (the codebase's standard DI-registration idiom,…","i":"AddModuleConferenceInfrastructure RemoveAllResilienceHandlers StandardResilienceHandler DependencyInjection HttpClient.Timeout IServiceCollection extension"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#how-it-fits-together-at-runtime","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"How it fits together at runtime","x":"Three flows tie the chapter together. Persistence flow: a Conference command handler mutates an aggregate and the unit of work saves; that resolves the concrete…","i":"Microsoft.EntityFrameworkCore.Metadata.Builders System.Text.Json.Serialization.JsonPropertyName CategoryInvariants.CategoryItemNameMaxLength MMCA.ADC.Conference.Infrastructure.Services Microsoft.Extensions.DependencyInjection MMCA.ADC.Migrations.SqlServer.Conference ApplyConfigurationsForEntitiesInContext SessionInvariants.AnswerValueMaxLength SpeakerInvariants.AnswerValueMaxLength QuestionInvariants.ManualIdRangeStart SpeakerQuestionAnswerConfiguration.cs EventInvariants.AnswerValueMaxLength"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","x":"This chapter is the edge of the Conference bounded context, the layer that turns the rich Conference domain (G17) and its CQRS slices (G18) into a running HTTP + gRPC surface,…","i":"MMCA.ADC.Conference.Contracts MMCA.ADC.Conference.Service MMCA.ADC.Conference.API ConferenceModuleSeeder ConferenceModule Microservices Readiness Contract Vertical IModule Design Rubric"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-controller-hierarchy-almost-everything-is-inherited","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The controller hierarchy, almost everything is inherited","x":"The Conference API exposes sixteen controllers, and the striking thing about them is how little code each carries. They split into three structural families, all built on the…","i":"sessionquestionanswerscontroller conferencecategoriescontroller ConferenceCategoriesController eventquestionanswerscontroller sessioncategoryitemscontroller speakercategoryitemscontroller SessionSelectionController sessionspeakerscontroller categoryitemscontroller eventspeakerscontroller PagedCollectionResult ServiceInfoController"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#authorization-at-the-edge-three-shapes-not-one","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Authorization at the edge, three shapes not one","x":"Authorization is capability-based by default but not uniform, and the differences are the interesting part. Most write-bearing controllers carry a class-level…","i":"AuthorizationPolicies.RequireAuthenticated ConferencePermissions.SpeakersManage SessionQuestionAnswersController EventQuestionAnswersController CurrentUserServiceExtensions IsPrivilegedConferenceReader AddModuleConferenceAPI ConferenceReadAudience HasPermissionAttribute SessionSelectionManage ConferencePermissions ICurrentUserService"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-request-records-the-inbound-write-shapes","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The request records, the inbound write shapes","x":"Several controllers declare small record class request types alongside themselves, co-located in the same file: AddRoomRequest/UpdateRoomRequest…","i":"updatesessionquestionanswerrequest updateeventquestionanswerrequest addsessionquestionanswerrequest addeventquestionanswerrequest addsessioncategoryitemrequest addspeakercategoryitemrequest updatecategoryitemrequest addsessionspeakerrequest addcategoryitemrequest addeventspeakerrequest SessionCreateRequest SessionsController"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#where-the-generic-shape-gives-way-filtering-warnings-and-calendars","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Where the generic shape gives way: filtering, warnings, and calendars","x":"SessionsController is the best illustration of how a controller earns its overrides. Every read action is [AllowAnonymous] and [OutputCache(PolicyName = \"SessionsCache\")]…","i":"BuildPublicSessionSpecificationAsync BuildPagedSessionSpecificationAsync GetSessionsBySpeakerFilterQuery GetPublicSessionFilterQuery GetPublicSponsorFilterQuery PublishedEventSpecification ExportSessionCalendarQuery EvictSessionsCacheAsync UpdateSponsorCommand HasDateRangeWarning IdempotentAttribute IOutputCacheFeature"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#two-more-deviations-versioning-and-decision-support","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Two more deviations, versioning and decision support","x":"ServiceInfoController exists to prove the API-versioning machinery works beyond a single version ([Rubric §9, API & Contract Design]). It is a one-member shell over Common's…","i":"ConferencePermissions.SessionSelectionManage SessionScoringEnqueueResult SessionSelectionController ServiceInfoControllerBase SessionScoringProcessor ServiceInfoController ISessionScoringQueue minimumSimilarity ConferenceCache AllowAnonymous AlreadyPending HandleFailure"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-module-entry-point-and-seeder-how-conference-plugs-in","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The module entry point and seeder, how Conference plugs in","x":"ConferenceModule is the Conference implementation of IModule. It is tiny by design: Register(...) calls the DependencyInjection extension's AddConferenceModule(...)…","i":"DisabledSessionBookmarkValidationService DisabledEventLiveValidationService ISessionBookmarkValidationService IEventLiveValidationService ConferenceErrorResources ConferenceModuleDbSeeder ConferenceModuleSeeder AuthorizationPolicies RegisterDisabledStubs RequireAuthenticated AddConferenceModule DependencyInjection"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-grpc-edge-conference-as-both-server-and-client","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The gRPC edge, Conference as both server and client","x":"When Conference is extracted into its own process, two of its in-process collaborations must cross a network boundary, and both are handled by the G13 transport boundary (Result…","i":"SessionBookmarkValidationServiceGrpcAdapter AddConferenceEventLiveValidationClient eventlivevalidationservicegrpcadapter AddConferenceSessionValidationClient ISessionBookmarkValidationService AddEngagementBookmarkCountClient ModuleLoader.DiscoverAndRegister eventlivevalidationgrpcservice GrpcResultExceptionInterceptor MMCA.ADC.Conference.Contracts MMCA.ADC.Conference.Service SessionBookmarksGrpcService"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-service-host-kestrel-first-and-why","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The service host: Kestrel first, and why","x":"The MMCA.ADC.Conference.Service Program.cs boots only the Conference module (Modules:Conference:Enabled=true). Kestrel is configured before anything else, and the whole of it is…","i":"builder.ConfigureEndpointsWithHealthProbe MMCA.ADC.Conference.Scoring MMCA.ADC.Conference.Service KestrelEndpointExtensions HttpProtocols.Http2 MapDefaultEndpoints HTTP_1_1_REQUIRED Http1AndHttp2 Program.cs UseSerilog httpGet GOAWAY"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#output-caching-and-warm-up-the-two-performance-extension-points","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Output caching and warm-up, the two performance extension points","x":"Output caching is where this host carries the most bespoke configuration (Program.cs:191-255). The base policy is deny-by-default NoCache (Program.cs:198), so only explicitly…","i":"ConferenceReadAudience.PrivilegedRoles PublicEndpointOutputCachePolicy SelfHttpOutputCacheWarmupTask DbUpdateConcurrencyException SessionSelectionController ConferenceErrorResources AddPublicEndpointPolicy SelfHttpWarmupTaskBase ConferencePublicCache BookmarkCountsCache AddErrorResources Event.Name.Empty"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-runtime-picture-one-host-two-transports","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The runtime picture, one host, two transports","x":"After module discovery (Program.cs:313-319) the host wires the Engagement gRPC client (Program.cs:329), the broker (AddBrokerMessaging registering the UserRegistered…","i":"MMCA.Common.Application.Interfaces.Infrastructure currentUserService.IsPrivilegedConferenceReader ConferencePermissions.SessionSelectionManage AuthorizationPolicies.RequireAuthenticated ConferenceReadAudience.PrivilegedRoles.Any builder.ConfigureEndpointsWithHealthProbe DisabledSessionBookmarkValidationService MMCA.ADC.Conference.Shared.Authorization ConferencePermissions.ContentManagement GetPublicSessionCategoryItemFilterQuery GetPublicSpeakerCategoryItemFilterQuery AddConferenceEventLiveValidationClient"},{"u":"/docs/onboarding/group-21-conference-ui.html","d":"21. ADC Conference - UI","k":"Onboarding Guide","x":"What this chapter covers. This is the consumer half of the \"write-once UI, render everywhere\" story (primer §2): the Blazor pages and per-page HTTP services that turn the…","i":"MMCA.ADC.Conference.UI Architecture Responsive Component Design Rubric"},{"u":"/docs/onboarding/group-21-conference-ui.html#the-layering-inside-the-ui-a-page-never-touches-httpclient","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"The layering inside the UI: a page never touches HttpClient","x":"Each page is a .razor + .razor.cs code-behind pair that depends only on a UI service interface, never on HttpClient and never on the API's internals. The eight CRUD-shaped…","i":"IConferenceCategoryUIService RefreshFromSessionizeAsync ConferenceCategoryService EnsureSuccessStatusCode ICategoryItemUIService ServiceExceptionHelper PagedCollectionResult SponsorIdentifierType CategoryItemService IQuestionUIService EntityServiceBase ISessionUIService"},{"u":"/docs/onboarding/group-21-conference-ui.html#the-list-pages-derive-from-datagridlistpagebasetdto-get-everything-for-free","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"The list pages: derive from DataGridListPageBase, get everything for free","x":"Ten list screens, the organizer EventList, SessionList, SpeakerList, ConferenceCategoryList, QuestionList, RoomList, SponsorList, and the public PublicEventList,…","i":"MobileInfiniteScrollList ConferenceCategoryList DataGridListPageBase PublicSessionList PublicSpeakerList PublicSponsorList FetchMobilePage ListPageActions PublicEventList LoadServerData RestoreFilters GetPagedAsync"},{"u":"/docs/onboarding/group-21-conference-ui.html#container-and-presentational-split","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Container and presentational split","x":"The behaviour-heavy screens do not keep everything in one code-behind: the page stays the container (data fetching, filter and paging state, service calls) and hands rendering to…","i":"SessionSelectionSpeakerOverlap PublicSessionListFilterBar SpeakerCategoryItemsPanel SessionSelectionAiScores SessionSelectionDisplay PublicSessionListView PublicSessionList Architecture ReloadAsync Changed Testing Rubric"},{"u":"/docs/onboarding/group-21-conference-ui.html#child-and-join-entities-a-thin-postdelete-base","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Child-and-join entities: a thin POST/DELETE base","x":"Sessions, speakers, and events own join relationships (a speaker added to a session, a category item to a speaker) that the generic CRUD base cannot model, because the write…","i":"ISessionCategoryItemUIService ISpeakerCategoryItemUIService SessionCategoryItemService SpeakerCategoryItemService ISessionSpeakerUIService ChildEntityServiceBase IEventSpeakerUIService SessionSpeakerService EventSpeakerService MMCA.Common.UI DeleteAsync Validation"},{"u":"/docs/onboarding/group-21-conference-ui.html#display-enrichment-lookups-the-getall-vs-getbyid-populator-gap-worked-around-in-the-ui","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Display-enrichment lookups: the GetAll-vs-GetById populator gap, worked around in the UI","x":"Because the API's list endpoints do not always populate every cross-entity navigation, several pages need a cheap id-to-name map to render speaker names beside a session or an…","i":"ICategoryItemLookupService CategoryItemLookupService ISpeakerLookupService SpeakerLookupService SponsorshipPacketUrl IEventLookupService EventLookupService PublicSessionList CategoryItemInfo SessionSpeakers SpeakerInfo Dictionary"},{"u":"/docs/onboarding/group-21-conference-ui.html#three-feature-areas-that-go-beyond-crud","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Three feature areas that go beyond CRUD","x":"First, the speaker self-service dashboard: SpeakerDashboard is gated on the speakerid JWT claim (read from the cascaded authentication state and parsed as a Guid,…","i":"IOrganizerSessionFeedbackUIService IOrganizerEventFeedbackUIService OrganizerSessionFeedbackService OrganizerEventFeedbackService ISpeakerDashboardUIService AuthenticatedServiceBase OrganizerSessionFeedback SpeakerDashboardService OrganizerEventFeedback ServiceExceptionHelper IPublicLinkBuilder SpeakerDashboard"},{"u":"/docs/onboarding/group-21-conference-ui.html#session-selection-decision-support-the-asynchronous-edge","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Session-selection decision support, the asynchronous edge","x":"The most behaviour-rich page is the organizer-only SessionSelectionDashboard, which renders category distribution, speaker overlap, locality breakdown, and AI content-similarity…","i":"SessionSelectionFilterOptions ScoreEventSessionsResultDTO ISessionSelectionUIService ScoreEventSessionsCommand SessionSelectionDashboard SessionSelectionService CurrentEventSelector ScorePollTracker ScorePollSignal SessionsScored Resilience inherited"},{"u":"/docs/onboarding/group-21-conference-ui.html#public-versus-authenticated-rendering-and-the-device-capability-path","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Public versus authenticated rendering, and the device-capability path","x":"A recurring [Rubric §11, Security] pattern: the same conference entity is exposed through two page families. The public family (PublicEventList/PublicEventDetail,…","i":"IServiceProvider.GetService IConnectivityStatusService ISessionBookmarkUIService ConferenceReadAudience IHapticFeedbackService CurrentEventDefaults PublicSessionDetail PublicSpeakerDetail IScreenshotService CachedSessionPage PublicEventDetail PublicSessionList"},{"u":"/docs/onboarding/group-21-conference-ui.html#sponsors-a-feature-area-in-miniature","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Sponsors, a feature area in miniature","x":"The sponsor surface is worth reading as a compact tour of every pattern above, because it is the newest and touches all of them. Organizers manage the roster through SponsorList…","i":"ADCSponsorCollectionResult DataGridListPageBase EventLookupService PublicSponsorList ADCSponsorInfo Enum.GetValues SponsorCreate SponsorDetail SponsorList SponsorTier SponsorDTO ADCHome"},{"u":"/docs/onboarding/group-21-conference-ui.html#the-landing-page","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"The landing page","x":"ADCHome is the conference front door, shared by the web and MAUI heads; both serve the editorial images from their own site root today, so neither overrides the ImageBasePath…","i":"CurrentEventSelector ADCCollectionResult ConferenceTrackInfo KeynoteSpeakerInfo ImageBasePath ADCEventInfo Performance EventPhase Rendering ADCHome Rubric Timer"},{"u":"/docs/onboarding/group-21-conference-ui.html#routes-and-navigation","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Routes and navigation","x":"All paths are centralized in ConferenceRoutePaths, a static catalogue of literal routes and id-parameterized builder methods (EventDetails(id), PublicSessionDetails(id),…","i":"ConferenceRoutePaths.EventDetails NavigationManager.NavigateTo NavigationPublicLinkBuilder EventFeedbackOrganizer ConferenceRoutePaths Internationalization PublicSessionDetails IPublicLinkBuilder IStringLocalizer SponsorVisitLink RoomCheckInLink SponsorDetails"},{"u":"/docs/onboarding/group-21-conference-ui.html#how-it-all-plugs-into-the-shell","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"How it all plugs into the shell","x":"Two registration types wire the area in. ConferenceUIModule implements Common's IUIModule (the front-end counterpart of the IModule back-end contract): it declares the module's…","i":"MMCA.ADC.Conference.UI.Pages.ConferenceCategory ConferenceRoutePaths.SessionSelectionDashboard MMCA.ADC.Conference.UI.Pages.SessionSelection ListPageActions.DeleteWithConfirmationAsync ArgumentException.ThrowIfNullOrWhiteSpace CurrentEventDefaults.SelectCurrentOrNext CurrentEventSelector.SelectCurrentOrNext DashboardService.GetSpeakerSessionsAsync ListPageActions.ReloadActiveLayoutAsync MMCA.ADC.Conference.UI.Pages.Feedback MMCA.ADC.Conference.UI.Pages.Question MMCA.ADC.Conference.UI.Pages.Session"},{"u":"/docs/onboarding/group-22-engagement-module.html","d":"22. ADC Engagement Module (Session Bookmarks)","k":"Onboarding Guide","x":"What this group covers. Engagement is the ADC bounded context that holds everything an attendee does at the conference beyond browsing the catalog. Four capability families live…","i":"MMCA.ADC.Engagement.Application.CheckIns.Services BookmarkCountService.BookmarkCountServiceClient MMCA.ADC.Engagement.Application.Points.Services MMCA.ADC.Engagement.Domain.UserSessionBookmarks MMCA.ADC.Engagement.Shared.UserSessionBookmarks MMCA.ADC.Engagement.Domain.Points.DomainEvents BookmarkCountService.BookmarkCountServiceBase MMCA.ADC.Engagement.Application.CheckIns.DTOs UserSessionBookmarkCacheEvictionHandlerTests assemblyProvider.GetConfigurationAssemblies AuthorizationPolicies.RequireAuthenticated CheckInsController.GetAttendanceStatsAsync"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","x":"What this chapter covers. This is the conference-day layer of the Engagement bounded context: the features that only matter while an event is actually happening in the room.…","i":"SessionQuestion PresenterView HappeningNow SessionLive LivePoll"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#the-two-aggregates-and-their-invariants","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"The two aggregates and their invariants","x":"Both aggregates are sealed AuditableAggregateRootEntity subclasses that follow the framework's factory-plus-Result discipline (primer §2). LivePoll…","i":"AuditableAggregateRootEntity SessionQuestion.Create SessionQuestionChanged SessionQuestionUpvote ToggleUpvoteHandler LivePollInvariants DomainEntityState LiveWindowEndUtc BaseDomainEvent CanAcceptUpvote CastVoteHandler LivePollChanged"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#the-write-path-and-where-the-realtime-broadcast-actually-happens","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"The write path, and where the realtime broadcast actually happens","x":"Each operation is a vertical slice under Application/{LivePollsSessionQuestions}/UseCases/{Op}/, and every command handler shares the first two beats: mutate the aggregate…","i":"SessionQuestionUpvoteChangedHandler ILiveChannelPublishQueue.Enqueue SessionQuestionUpvoteChanged LiveChannelPublishWorkItem LivePollVoteChangedHandler ILiveChannelPublishQueue ModerateQuestionHandler SessionQuestionChannel CreateLivePollHandler LivePollClosedPayload SubmitQuestionHandler CloseLivePollHandler"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#one-websocket-one-publisher-port-and-a-cross-service-ingress","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"One WebSocket, one publisher port, and a cross-service ingress","x":"The transport itself is framework-owned (ADR-039, Group 10). The single NotificationHub carries both durable notifications and channel events on one connection, and the…","i":"LiveChannelPublisherGrpcAdapter LiveChannelPublishProcessor SignalRLiveChannelPublisher RendererInfo.IsInteractive NullLiveChannelPublisher IPushNotificationSender LiveChannelGrpcService NotificationHubService ILiveChannelPublisher OnAfterRenderAsync OnInitializedAsync LeaveChannelAsync"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#the-read-path-and-how-the-ui-reacts","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"The read path and how the UI reacts","x":"Reads do not go through the generic entity-query machinery; the live views need shaped projections. LivePollResultsBuilder…","i":"LivePollNavigationPopulator SessionLiveModerationPanel SessionQuestionViewBuilder SessionLiveQuestionPanel LivePollResultsBuilder ISessionLiveUIService LivePollConfiguration CurrentEventSelector SessionLivePollPanel SessionLiveUIService GetOpenPollsHandler LivePollDTOMapper"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#authorization-feature-gating-and-the-cross-service-dependency-on-conference","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"Authorization, feature gating, and the cross-service dependency on Conference","x":"Both controllers, LivePollsController (MMCA.ADC.Engagement.API/Controllers/LivePollsController.cs:42) and SessionQuestionsController…","i":"MMCA.ADC.Engagement.Domain.LivePolls.DomainEvents MMCA.ADC.Engagement.Application.LivePolls.DTOs SessionQuestionChannel.QuestionUpvoteChanged MMCA.ADC.Engagement.Domain.SessionQuestions MMCA.ADC.Engagement.Shared.SessionQuestions AuthorizationPolicies.RequireAuthenticated LivePollInvariants.EnsureOptionTextIsValid PushNotificationSettings.ChannelKeyPattern MMCA.ADC.Engagement.UI.Pages.HappeningNow SessionQuestionPendingCountChangedPayload SessionQuestionUpvote.QuestionId.Required CurrentEventSelector.SelectCurrentOrNext"},{"u":"/docs/onboarding/group-24-identity-module.html","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","x":"What this chapter covers. This is the Identity bounded context of MMCA.ADC, the module that owns who a person is across every ADC surface: web, WebAssembly, and MAUI. It is a…","i":"GetUserPreferencesHandlerBase AuditableAggregateRootEntity AuthenticationServiceBase ChangePasswordHandlerBase ExportUserDataHandlerBase HasPermissionAttribute DeleteUserHandlerBase BaseIntegrationEvent SoftDeletedUserCache TIdentifierType IAnonymizable PiiAttribute"},{"u":"/docs/onboarding/group-24-identity-module.html#projects-one-bounded-context","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Projects, one bounded context","x":"The module is split along the standard Clean Architecture layering ([Rubric §3, Clean Architecture]), each project pinned by a trivial AssemblyReference / ClassReference anchor…","i":"MMCA.ADC.Identity.Infrastructure MMCA.ADC.Identity.Application ScanModuleApplicationServices MaxRegistrationsPerIpPerHour MMCA.ADC.Identity.Contracts ModuleApplicationDbContext MMCA.ADC.Identity.Service MMCA.ADC.Identity.Domain MMCA.ADC.Identity.Shared SoftDeletedUserValidator IdentityErrorResources IdentityModuleDbSeeder"},{"u":"/docs/onboarding/group-24-identity-module.html#the-user-aggregate-credentials-profile-and-cross-context-links-in-one-root","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"The User aggregate: credentials, profile, and cross-context links in one root","x":"User (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Domain/Users/User.cs:33) is the only aggregate root in the module, and it carries more responsibility than most: it is…","i":"RegisterRequestValidator IPasswordChangeableUser DeviceFieldMaxLength UserPasswordChanged FirstNameMaxLength RefreshTokenExpiry RevokeRefreshToken UpdateRefreshToken LastNameMaxLength UpdatePreferences UserConfiguration CommonInvariants"},{"u":"/docs/onboarding/group-24-identity-module.html#authentication-a-thin-subclass-over-the-shared-engine","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Authentication: a thin subclass over the shared engine","x":"The login / registration / refresh / revocation workflow is not re-implemented here. It lives in AuthenticationServiceBase (G08), which owns the validate-first flow, the lockout…","i":"HttpContextExternalLoginEmailVerifier UnitOfWork.ExecuteInTransactionAsync CreateChangePreferencesCommand Auth.ExternalEmailNotVerified IdentityPermissions.UsersRead UserAccountAuthControllerBase CreateChangePasswordCommand IExternalLoginEmailVerifier AuthenticationServiceBase GetUserPreferencesHandler TChangePreferencesCommand ChangePreferencesCommand"},{"u":"/docs/onboarding/group-24-identity-module.html#the-privacy-pair-export-and-erasure","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"The privacy pair: export and erasure","x":"Two use cases make this module the codebase's clearest [Rubric §30, Compliance / Privacy / Data Governance] story, and both are now thin ADC specializations of a G14 base. The…","i":"UserDataExportEngagementSectionDTO NotificationUserDataExportSection EngagementUserDataExportSection IUserNotificationExportService IUserEngagementExportService BuildSubjectSnapshotAsync ExportUserDataHandlerBase UserDataExportSectionDTO UserDataExportSubjectDTO IUserDataExportSection OnAfterSoftDeleteAsync DeleteUserHandlerBase"},{"u":"/docs/onboarding/group-24-identity-module.html#avatars-the-third-mutating-slice","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Avatars: the third mutating slice","x":"The avatar trio is a small but complete example of a file-handling slice ([Rubric §11, Security] at the content boundary, ADR-045). UsersController caps the multipart upload at 2…","i":"RemoveUserAvatarHandler Avatar.InvalidUpload GetUserAvatarHandler SetUserAvatarHandler IFileStorageService ImageContentSniffer RequestSizeLimit IImageProcessor UsersController MaxAvatarBytes"},{"u":"/docs/onboarding/group-24-identity-module.html#persistence-seeding-and-the-disabled-stub","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Persistence, seeding, and the disabled stub","x":"ModuleApplicationDbContext (ModuleApplicationDbContext.cs:15) is the abstract, engine-agnostic context declaring the single Users set (:22); the concrete per-engine class…","i":"EntityTypeConfigurationSQLServer DisabledAttendeeQueryService IdentityModuleDbSeederBase ModuleApplicationDbContext IdentityModuleDbSeeder RegisterDisabledStubs ApplicationDbContext IdentityModuleSeeder EmailValueConverter dbo.OutboxMessages SQLServerDbContext UserConfiguration"},{"u":"/docs/onboarding/group-24-identity-module.html#crossing-the-service-boundary-grpc-and-integration-events","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Crossing the service boundary: gRPC and integration events","x":"Identity talks to its peers two ways, and both live in Shared and Contracts so neither side reaches into the other's domain ([Rubric §7, Microservices Readiness]). Synchronously,…","i":"ConfigureEndpointsWithHealthProbe ModuleLoader.DiscoverAndRegister AttendeeQueryServiceGrpcAdapter SpeakerUnlinkedFromUserHandler SpeakerLinkedToUserHandler AddIdentityAttendeeClient KestrelEndpointExtensions RequireSuccessStatusCode SpeakerUnlinkedFromUser SelfHttpWarmupTaskBase AuthenticationService IAttendeeQueryService"},{"u":"/docs/onboarding/group-24-identity-module.html#the-ui-edge","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"The UI edge","x":"The Blazor surface is registered as an IdentityUIModule (MMCA.ADC.Identity.UI/IdentityUIModule.cs:13), an IUIModule descriptor that contributes two NavItems as resource keys, \"My…","i":"AuthenticatedServiceBase MobileInfiniteScrollList RetryPolicy.ExecuteAsync MMCA.Common.Testing.E2E DataGridListPageBase DependencyInjection IMediaPickerService IdentityRoutePaths IdentityUIModule ListPageActions IUserUIService UserListDTO"},{"u":"/docs/onboarding/group-24-identity-module.html#end-to-end-one-registration","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"End-to-end: one registration","x":"To see the chapter cooperate, follow a new attendee signing up. AuthController receives the register POST, captures the client IP for BR-213 rate limiting (AuthController.cs:57),…","i":"MMCA.ADC.Identity.Shared.Users.IntegrationEvents AttendeeQueryService.AttendeeQueryServiceClient System.Diagnostics.CodeAnalysis.SuppressMessage MMCA.ADC.Identity.Application.Users.Validation AttendeeQueryService.AttendeeQueryServiceBase AuthStateProvider.GetAuthenticationStateAsync LoginProtection__MaxRegistrationsPerIpPerHour ServiceCollectionDescriptorExtensions.Replace ListPageActions.DeleteWithConfirmationAsync MMCA.ADC.Identity.Domain.Users.DomainEvents ExternalAuthExtensions.ExternalLoginScheme System.Collections.Frozen.FrozenDictionary"},{"u":"/docs/onboarding/group-25-adc-host-composition.html","d":"25. ADC Application Host, UI Shell & Cross-Module Composition","k":"Onboarding Guide","x":"What this chapter covers. Every ADC module described so far, Conference, Engagement, Identity, Notification, is consumed somewhere. This chapter is that somewhere: the client…","i":"Microsoft.Extensions.Configuration.IConfiguration ArgumentException.ThrowIfNullOrWhiteSpace NowNextWidgetProvider.FetchSnapshotAsync MMCA.Common.UI.Components.Capabilities IPlatformApplication.Current.Services UIModuleConfiguration.IsModuleEnabled RemoteCertificateValidationCallback SessionCookieAuthenticationHandler EngagementRoutePaths.HappeningNow NowNextWidgetProvider.BuildViews System.Resources.ResourceManager WebAuthenticatorCallbackActivity"},{"u":"/docs/onboarding/group-26-device-capability-layer.html","d":"26. Device Capability Abstraction Layer (Native Contracts, MAUI, Browser & Fallback Adapters)","k":"Onboarding Guide","x":"What this chapter covers. One Blazor component library in MMCA.Common.UI renders on three very different heads: Blazor Server (server-side prerender plus interactive Server…","i":"MauiBackNavigationBridge.HandleBackPressedAsync MMCA.Common.UI.Services.Capabilities.Fallbacks MMCA.Common.UI.Services.Capabilities.Browser builder.Services.AddMauiDeviceCapabilities MauiLocalNotificationService.ScheduleAsync WebAuthenticator.Default.AuthenticateAsync ArgumentException.ThrowIfNullOrWhiteSpace Battery.Default.EnergySaverStatusChanged CommunityToolkit.Maui.Media.SpeechToText Connectivity.Current.ConnectivityChanged CultureInfo.DefaultThreadCurrentCulture ILocalNotificationService.ScheduleAsync"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","x":"What this group covers. Everything the codebase uses to prove itself: the four reusable test-support packages that ship out of MMCA.Common/Source/Hosting (MMCA.Common.Testing,…","i":"ServiceInfoVersioningContractTestsBase SqlServerIntegrationTestFixtureBase MMCA.Common.Testing.Architecture ProductionHostApplicationFactory AccessibilityViolationException DecoratorPipelineOrderTestsBase FeatureManagementTestExtensions ProblemDetailsContractTestsBase CapturingHttpMessageHandler CrossEntityNavigationFinder RouteAuthorizationTestsBase BunitInteractionExtensions"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#integration-tests-a-real-host-a-throwaway-database-a-per-test-reset","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Integration tests: a real host, a throwaway database, a per-test reset","x":"The integration tier boots the actual application, not a mock of it. The abstraction at its center is IIntegrationTestFixture (MMCA.Common.Testing/IIntegrationTestFixture.cs:8):…","i":"SqlServerIntegrationTestFixtureBase ProductionHostApplicationFactory FeatureManagementTestExtensions appsettings.Development.json SqlBaseEnvironmentVariable ConfigureTestFeatureFlags IEntityDataSourceRegistry CrossServiceFixtureBase IIntegrationTestFixture CrossServiceDataSource __EFMigrationsHistory WebApplicationFactory"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#architecture-fitness-functions-rules-that-gate-the-build","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Architecture fitness functions: rules that gate the build","x":"The layering and DDD conventions this codebase commits to are not left to code review, they are executed as tests. The reusable rule library lives in…","i":"CancellationTokenConventionTestsBase ConstructorDependencyCountTestsBase IntegrationEventContractTestsBase MMCA.Common.Testing.Architecture ObservabilityConventionTestsBase AggregateRootsHaveResultFactory MicroserviceExtractionTestsBase RawQueryableConventionTestsBase IdempotencyConventionTestsBase ArchitectureRules.Entities.cs AggregateConventionTestsBase CrossEntityNavigationFinder"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#component-tests-real-mudblazor-faked-edges","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Component tests: real MudBlazor, faked edges","x":"The bUnit tier renders a single Blazor component in-process with its real dependencies but stubbed network and auth. BunitComponentTestBase…","i":"IsAuthenticatedAuthorizationService AuthenticationStateProvider CapturingHttpMessageHandler BunitInteractionExtensions StubTokenStorageService BunitComponentTestBase FreshApiClientFactory MarkupSnapshotResult UiHttpServiceHarness AuthenticationState HttpMessageHandler IRenderedComponent"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#end-to-end-tests-a-real-browser-accessibility-and-performance-as-gates","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"End-to-end tests: a real browser, accessibility and performance as gates","x":"The E2E tier drives a real browser through Playwright. PlaywrightFixture (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6) is an xUnit collection fixture (its…","i":"AssertNoAccessibilityViolationsAsync AccessibilityViolationException Wcag21AaExceptMudPagerCombobox ProfileManagementTestsBase GotoAndWaitForBlazorAsync UserRegistrationTestsBase UserPreferencesTestsBase ClickAndWaitForUrlAsync window.Blazor._internal AuthorizationTestsBase WaitForAuthResultAsync AuthenticatedUserPath"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#the-gallery-harness","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"The Gallery harness","x":"Component and E2E coverage of MMCA.Common's own UI needs a page to render, but the framework is not a runnable app. MMCA.Common.UI.Gallery is a deliberately backend-less Blazor…","i":"GalleryAuthenticationStateProvider GalleryFakeAuthenticationHandler StubNotificationInboxUIService StubPushNotificationUIService MMCA.Common.UI.E2E.Tests NullTokenStorageService MMCA.Common.UI.Gallery MapRazorComponents NullTokenRefresher NoOpAuthUIService MMCA.Common.slnx GalleryUIModule"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#contract-pipeline-and-benchmark-bases","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Contract, pipeline, and benchmark bases","x":"The last family pins guarantees that live in the composition of the stack rather than in any one type, and it is the subject of ADR-058: these suites ship in MMCA.Common.Testing…","i":"Application_ShouldNotDependOn_EntityFrameworkCore Controllers_ShouldNotDependOn_EntityFrameworkCore DataSubject_DeclaresPii_SoTheContractIsNotVacuous MMCA.Common.Architecture.Tests.CycleFixtures.Left Module_ShouldDeclare_ExpectedRequiresDependencies PiiRedactor_LeaksNoClearTextPii_ToLogsOrTelemetry CultureSwitch_ToSpanish_ShouldLocalizeAndPersist EveryRunbookAlertSection_MapsToAProvisionedAlert MobileViewport_CultureAndTheme_ShouldBeReachable ModuleShared_ShouldNotDependOn_OwnInternalLayers OpenApiDocument_DescribesEveryCorePublicResource Register_WithMismatchedPasswords_ShouldShowError"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#per-project-test-rollup","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Per-project test rollup","x":"This guide treats tests as grouped, not sectioned per [Fact] (the logged exception in the charter): the reusable test bases, the shared architecture-fitness library and its…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests UserSessionBookmarkCacheEvictionHandlerTests PushNotificationProjectionTranslationTests SpecificationsDoNotNavigateToOtherEntities CachingDecoratorConstructorSelectionTests CurrentUserTargetingContextAccessorTests Microsoft.Extensions.DependencyInjection MMCA.ADC.Conference.Infrastructure.Tests MMCA.ADC.Engagement.Infrastructure.Tests MMCA.ADC.Notification.Application.Tests EntityServiceBaseIdempotencyRetryTests MMCA.ADC.CrossService.IntegrationTests"},{"u":"/docs/onboarding/devops-aspire.html","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","x":"This chapter teaches how the MMCA.ADC system goes from a single dotnet run on your workstation to a running stack of six .NET processes plus four containers: databases, a broker,…","i":"MMCA.Common.Aspire ServiceDefaults WithReference dotnet run"},{"u":"/docs/onboarding/devops-aspire.html#the-one-command-local-run","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"The one-command local run","x":"That command brings up everything the application needs locally: four SQL Server databases, Redis, RabbitMQ with management UI, a MailDev SMTP interceptor, four extracted…"},{"u":"/docs/onboarding/devops-aspire.html#mmcaadcapphost-the-orchestration-project","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"MMCA.ADC.AppHost, the orchestration project","x":"Source file: MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs Extension helpers: MMCA.Common.Aspire.Hosting/Extensions.cs (AddMessageBroker, WithBroker, WithJwksDiscovery,…","i":"identityService.WithE2eRegistrationThrottleLift LoginProtection__MaxRegistrationsPerIpPerHour ConnectionStrings__SQLServerConnectionString ConnectionStrings__CosmosConnectionString ConnectionStrings__SqliteConnectionString Authentication__JwtBearer__Authority identityService.WithEnvironment services__notification__grpc__0 WithE2eRegistrationThrottleLift E2E_LIFT_REGISTRATION_THROTTLE GrpcResultExceptionInterceptor JwtForwardingClientInterceptor"},{"u":"/docs/onboarding/devops-aspire.html#where-service-defaults-come-from-mmcacommonaspire-not-a-local-project","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Where service defaults come from, MMCA.Common.Aspire, not a local project","x":"There is no MMCA.ADC.ServiceDefaults project. The conventional Aspire \"ServiceDefaults\" shared project that scaffolding generates has been deleted; each service host (and the UI)…","i":"AddCommonKeyVaultConfiguration scoring.run.failed.terminal MMCA.ADC.ServiceDefaults AddCommonDataProtection DefaultAzureCredential builder.Configuration AuditTrailCleanupJob ConfigurationManager MapDefaultEndpoints AddServiceDefaults MMCA.Common.Aspire ScheduledJobRunner"},{"u":"/docs/onboarding/devops-aspire.html#mmcacommonaspire-the-framework-service-defaults-package","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"MMCA.Common.Aspire, the framework service-defaults package","x":"Source: MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs Telemetry: MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Telemetry/OutboxPollFilterProcessor.cs Security:…","i":"APPLICATIONINSIGHTS_CONNECTION_STRING OpenIdConnectMetadataWarmupTask EnableMultipleHttp2Connections AddInfrastructureHealthChecks Services.AddServiceDiscovery Telemetry__TracesSampleRatio ActivityTraceFlags.Recorded ConfigureHttpClientDefaults OTEL_EXPORTER_OTLP_ENDPOINT PooledConnectionIdleTimeout MMCA.Common.Infrastructure WarmupReadinessHealthCheck"},{"u":"/docs/onboarding/devops-aspire.html#mmcacommonaspirehosting-the-apphost-extensions-package","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"MMCA.Common.Aspire.Hosting, the AppHost extensions package","x":"Source: MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs This package lives in a separate assembly from MMCA.Common.Aspire so running services do not pull in…","i":"identityService.WithE2eRegistrationThrottleLift LoginProtection__MaxRegistrationsPerIpPerHour Authentication__JwtBearer__Authority WithE2eRegistrationThrottleLift E2E_LIFT_REGISTRATION_THROTTLE E2eRegistrationsPerIpPerHour builder.AddMessageBroker E2E_JWT_PRIVATE_KEY_PEM WithSQLServerDataSource E2E_JWT_PUBLIC_KEY_PEM Jwks__RsaPublicKeyPem Jwt__RsaPrivateKeyPem"},{"u":"/docs/onboarding/devops-aspire.html#the-six-dockerfiles","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"The six Dockerfiles","x":"All six Dockerfiles share the same multi-stage structure (base → build → publish → final) and the same base images. None build the AppHost, it is a local-only orchestration…","i":"MMCA.ADC.Notification.Service.dll MMCA.ADC.Conference.Service.dll MMCA.ADC.Engagement.Service.dll GlobalUsings.IdentifierType.cs MMCA.ADC.Identity.Service.dll MMCA.ADC.UI.Web.Client Directory.Build.props TreatWarningsAsErrors MMCA.ADC.Gateway.dll MMCA.ADC.UI.Web.dll MMCA.Common.Aspire MMCA.ADC.UI.Web"},{"u":"/docs/onboarding/devops-aspire.html#local-to-cloud-parity","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Local-to-cloud parity","x":"The AppHost topology maps directly to the Azure infrastructure provisioned by infra/main.bicep. The table below cross-references the local resource with its Azure equivalent: The…","i":"ConnectionStrings__SQLServerMigrationsAssembly APPLICATIONINSIGHTS_CONNECTION_STRING __SQLServerMigrationsAssembly OTEL_EXPORTER_OTLP_ENDPOINT ConnectionStrings__redis WithSQLServerDataSource Outbox__DatabaseName AddBrokerMessaging MessageBusProvider ADC_Notification AzureServiceBus ADC_Conference"},{"u":"/docs/onboarding/devops-aspire.html#the-yarp-gateways-role","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"The YARP Gateway's role","x":"The gateway (Source/Hosts/MMCA.ADC.Gateway) is a pure YARP reverse proxy. It has no DbContext, no ModuleLoader, no REST controllers, and no broker connection. Its Program.cs is…","i":"HttpResilienceDefaults.TotalRequestTimeout notificationRestConfig HttpVersion.Version20 RequestVersionOrLower RequestVersionExact restActivityTimeout ActivityTimeout Http1AndHttp2 VersionPolicy ForwardHttp2 MapForwarder ModuleLoader"},{"u":"/docs/onboarding/devops-aspire.html#startup-ordering-summary","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Startup ordering summary","x":"The health-based WaitFor chain imposes this ordering. Note that three of the four services wait on Identity without any explicit WaitFor in the AppHost: WithJwksDiscovery adds it…","i":"WithJwksDiscovery WithReference WaitFor"},{"u":"/docs/onboarding/devops-aspire.html#not-determinable-from-source","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Not determinable from source","x":"- The specific integration events that flow over the broker (e.g., UserRegistered, SpeakerLinkedToUser) are cited from AppHost inline comments (Program.cs:46-51, 130-136), not…","i":"SpeakerLinkedToUser UserRegistered CLAUDE.md"},{"u":"/docs/onboarding/devops-cicd.html","d":"CI/CD and Operations","k":"Onboarding Guide","x":"This chapter walks the GitHub Actions workflows that govern MMCA, from the framework's continuous integration and lockstep NuGet release in MMCA.Common, through the ADC…","i":"MMCA.Common"},{"u":"/docs/onboarding/devops-cicd.html#mmcacommon-ciyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.Common, ci.yml","x":"File: MMCA.Common/.github/workflows/ci.yml The continuous-integration workflow for the MMCA.Common framework. Because the fifteen packages are consumed by every downstream…","i":"MMCA.Common.Infrastructure.Redis.Tests RestorePackagesWithLockFile Deque.AxeCore.Playwright Directory.Packages.props PLAYWRIGHT_BROWSERS_PATH DistributedCacheService MMCA.Common.Testing.E2E MMCA.Common.UI.Gallery Directory.Build.props TreatWarningsAsErrors Infrastructure.Tests MMCA.Common.UI.Tests"},{"u":"/docs/onboarding/devops-cicd.html#mmcacommon-releaseyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.Common, release.yml","x":"File: MMCA.Common/.github/workflows/release.yml The lockstep NuGet release workflow. When a maintainer pushes a vX.Y.Z git tag, this workflow deterministically derives the…","i":"Directory.Packages.props github.repository_owner DependencyVersionTests Testing.Architecture MMCA.Common.UI.Maui MMCA.Common.slnx GITHUB_REF_NAME Aspire.Hosting Infrastructure GITHUB_TOKEN Application release.yml"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-deployyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, deploy.yml","x":"File: MMCA.ADC/.github/workflows/deploy.yml The primary CI/CD pipeline for the Atlanta Developers Conference application. It runs on every push to main, on every pull request…","i":"needs.foundation.outputs.acrLoginServer coverage.integration.cobertura.xml MMCA.ADC.Integration.slnf Directory.Packages.props USE_MANAGED_IDENTITY_SQL JWT_RSA_PRIVATE_KEY_PEM MMCA.ADC.Services.Tests __EFMigrationsHistory Directory.Build.props SQL_LOCATION_OVERRIDE WebApplicationFactory skip_freshness_gates"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-e2eyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, e2e.yml","x":"File: MMCA.ADC/.github/workflows/e2e.yml The full-stack Playwright E2E test workflow. It brings up the complete Aspire stack (SQL Server + Redis + RabbitMQ + four services +…","i":"PLAYWRIGHT_BROWSERS_PATH MMCA.Common.Testing.E2E github.event.schedule WEB_VITALS_OUTPUT_DIR PlaywrightFixture workflow_dispatch matrix.browser WebVitalsTests workflow_call E2E_BASE_URL GITHUB_TOKEN E2E_BROWSER"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-cost-guardyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, cost-guard.yml","x":"File: MMCA.ADC/.github/workflows/cost-guard.yml A read-only FinOps check that confirms the production Azure footprint is at its cost baseline. It detects a specific operational…","i":"project_adc_2026_actual_load.md BASELINE_MAX_REPLICAS workflow_dispatch workflow_call deploy.yml production"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-load-testyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, load-test.yml","x":"File: MMCA.ADC/.github/workflows/load-test.yml A k6 load test targeting the output-cached Conference read endpoints through the production Gateway. It establishes a repeatable…","i":"project_adc_2026_actual_load.md workflow_dispatch inputs.peak_vus production base_url BASE_URL peak_vus PEAK_VUS"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-cutover-per-service-dbsyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, cutover-per-service-dbs.yml","x":"File: MMCA.ADC/.github/workflows/cutover-per-service-dbs.yml A one-time, manually-triggered workflow that migrated the four empty per-service databases (ADCIdentity,…","i":"inputs.freeze_traffic ADC_Notification OutboxProcessor ADC_Conference ADC_Engagement freeze_traffic OutboxMessages ADC_Identity containerapp GITHUB_TOKEN SqlBulkCopy deploy.yml"},{"u":"/docs/onboarding/devops-cicd.html#cross-workflow-summary","d":"CI/CD and Operations","k":"Onboarding Guide","t":"Cross-workflow summary","x":"(dr-drill.yml is the ADR-009 §29 restore drill: it PITR-restores a copy of a chosen database, times the restore for the RTO record, verifies it comes back Online, then deletes…","i":"workflow_call deploy.needs deploy.yml federated because e2e.yml subject deploy scoped false slnx the"},{"u":"/docs/onboarding/devops-cicd.html#rubric-category-index-for-this-chapter","d":"CI/CD and Operations","k":"Onboarding Guide","t":"Rubric category index for this chapter","i":"WebVitalsTests deploy.needs environment release.yml deploy.yml foundation production coverage cutover e2e.yml ci.yml deploy"},{"u":"/docs/onboarding/devops-iac.html","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","x":"This chapter teaches the Azure Infrastructure-as-Code layer for the MMCA.ADC application: what resources are provisioned, why they are shaped the way they are, how secrets reach…","i":"azure.yaml deploy.yml"},{"u":"/docs/onboarding/devops-iac.html#how-the-pieces-fit-together","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"How the pieces fit together","x":"Before diving into individual files, here is the end-to-end picture: Phases 1 and 2 are their own jobs (deploy.yml:747, deploy.yml:795) rather than steps inside deploy, so they…","i":"AZURE_RESOURCE_GROUP resourceGroup foundation main.bicep AtlDevCon deploy"},{"u":"/docs/onboarding/devops-iac.html#azureyaml-the-azd-project-definition","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"azure.yaml, the azd project definition","x":"File: MMCA.ADC/azure.yaml azure.yaml is the Azure Developer CLI (azd) manifest for the project. It declares six deployable services and points azd at the Bicep infrastructure…","i":"Directory.Packages.props foundation.bicep containerapp notification azure.yaml conference engagement main.bicep identity language provider context"},{"u":"/docs/onboarding/devops-iac.html#infrafoundationbicep-long-lived-shared-infrastructure","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"infra/foundation.bicep, long-lived shared infrastructure","x":"File: MMCA.ADC/infra/foundation.bicep Foundation is deployed first (CI/CD chapter: deploy.yml:773-779) on every run. It provisions three resources: the Azure Container Registry,…","i":"reference_log_analytics_sku_limits.md needs.foundation.outputs.acrName workspaceCapping.dailyQuotaGb appLogsConfiguration adminUserEnabled logAnalyticsName environmentName acrLoginServer resourceGroup resourceToken timerTriggers acrPurgeTask"},{"u":"/docs/onboarding/devops-iac.html#deployment-parameters-assembled-at-deploy-time-not-committed","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Deployment parameters, assembled at deploy time, not committed","x":"There is no infra/main.parameters.json file in the repository, the infra/ directory holds only foundation.bicep, main.bicep, DISASTER-RECOVERY.md, OPERATIONS.md,…","i":"USE_MANAGED_IDENTITY_SQL useManagedIdentitySql deploymentParameters SQL_ADMIN_PASSWORD alertEmailAddress foundation.bicep logAnalyticsName sqlAdminPassword environmentName Microsoft.Sql OPERATIONS.md hasAnthropic"},{"u":"/docs/onboarding/devops-iac.html#inframainbicep-the-full-application-infrastructure","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"infra/main.bicep, the full application infrastructure","x":"File: MMCA.ADC/infra/main.bicep main.bicep declares every application-layer Azure resource: Application Insights, the SLO scheduled query rules and their action group, two…","i":"ApplicationSettings__DatabaseInitStrategy Logging__OpenTelemetry__LogLevel__Default APPLICATIONINSIGHTS_CONNECTION_STRING AddHttpForwarderWithServiceDiscovery Authentication__JwtBearer__Authority project_outbox_cost_optimization.md Telemetry__DisableHttpClientMetrics project_adc_no_broker_in_azure.md Scheduler__PollingIntervalSeconds ObservabilityConventionTestsBase Telemetry__DisableRuntimeMetrics DataProtection__ApplicationName"},{"u":"/docs/onboarding/devops-iac.html#deployment-model-summary","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Deployment model summary","x":"The complete credential chain: No static credential exists at any link in this chain. The GitHub secrets AZURECLIENTID, AZURETENANTID, AZURESUBSCRIPTIONID are the OIDC…","i":"AZURE_SUBSCRIPTION_ID SQL_ADMIN_PASSWORD AZURE_CLIENT_ID AZURE_TENANT_ID secure"},{"u":"/docs/onboarding/devops-iac.html#rubric-category-cross-reference","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Rubric category cross-reference","x":"---","i":"useManagedIdentitySql OTEL_SERVICE_NAME adminUserEnabled KeyVault__Uri dailyQuotaGb minReplicas commonTags secrets secure false grpc"},{"u":"/docs/onboarding/devops-iac.html#not-determinable-from-source","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Not determinable from source","x":"- The exact AcrPull and Key Vault Secrets User role-assignment commands used in the out-of- band bootstrap are referenced in comments (main.bicep:915-919, main.bicep:933-936) but…","i":"USE_MANAGED_IDENTITY_SQL AZURE_RESOURCE_GROUP SQL_AAD_ADMIN_LOGIN AZURE_SQL_LOCATION SQL_AAD_ADMIN_OID deploymentMode deploy.yml main.bicep AcrPull Secrets westus2 false"},{"u":"/docs/onboarding/devops-runbooks.html","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","x":"This chapter covers every operational script and runbook in MMCA.ADC: the one-time Azure bootstrap, the database-per-service cutover story (how the legacy AtlDevCon monolith DB…","i":"MMCA.Store AtlDevCon MMCAStore MMCA.ADC ib_rg"},{"u":"/docs/onboarding/devops-runbooks.html#azure-setupsh-one-time-azure-bootstrap","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"azure-setup.sh, One-time Azure bootstrap","x":"File: MMCA.ADC/scripts/azure-setup.sh What it is. A bash script that creates every Azure identity and OIDC credential the GitHub Actions deploy pipeline needs. It is idempotent:…","i":"feedback_azure_cli_role_bug.md JWT_RSA_PRIVATE_KEY_PEM JWT_RSA_PUBLIC_KEY_PEM AZURE_SUBSCRIPTION_ID create_or_replace_fic AZURE_RESOURCE_GROUP MissingSubscription SQL_ADMIN_PASSWORD AZURE_CLIENT_ID AZURE_TENANT_ID Technologies assign_role"},{"u":"/docs/onboarding/devops-runbooks.html#the-database-per-service-cutover-story","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"The database-per-service cutover story","x":"Before the cutover scripts make sense, the story behind them does. Before ADR-006. All four modules (Identity, Conference, Engagement, Notification) pointed at a single shared…","i":"DataSources__Identity__SQLServerConnectionString CrossDataSourceDegradeConvention project_outbox_race_shared_db.md AtlDevCon.dbo.OutboxMessages inputs.freeze_traffic dbo.OutboxMessages workflow_dispatch ADC_Notification OutboxProcessor ADC_Conference ADC_Engagement freeze_traffic"},{"u":"/docs/onboarding/devops-runbooks.html#copy-atldevcon-to-per-service-dbsazureps1-azure-data-copy","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"copy-atldevcon-to-per-service-dbs.azure.ps1, Azure data copy","x":"File: MMCA.ADC/scripts/copy-atldevcon-to-per-service-dbs.azure.ps1 What it is. A PowerShell script that streams rows from AtlDevCon into the four per-service Azure SQL databases…","i":"Microsoft.Data.SqlClient AtlDevCon.schema.Table QUOTED_IDENTIFIER OutboxMessages KeepIdentity is_computed SqlBulkCopy sys.columns CHECKIDENT rowversion RowVersion AtlDevCon"},{"u":"/docs/onboarding/devops-runbooks.html#migrate-atldevcon-to-per-service-dbsps1-local-data-copy-wrapper","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"migrate-atldevcon-to-per-service-dbs.ps1, local data copy wrapper","x":"File: MMCA.ADC/scripts/migrate-atldevcon-to-per-service-dbs.ps1 What it is. A thin PowerShell wrapper that invokes the companion SQL script via sqlcmd against the local Aspire…","i":"QUOTED_IDENTIFIER AtlDevCon localhost sqlcmd error exit sql"},{"u":"/docs/onboarding/devops-runbooks.html#migrate-atldevcon-to-per-service-dbssql-local-sql-copy-script","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"migrate-atldevcon-to-per-service-dbs.sql, local SQL copy script","x":"File: MMCA.ADC/scripts/migrate-atldevcon-to-per-service-dbs.sql What it is. The T-SQL script that performs the actual per-row copy from AtlDevCon into the four per-service…","i":"AtlDevCon.sys.columns sys.identity_columns IDENTITY_INSERT OutboxMessages CHECKIDENT SchemaName XACT_ABORT AtlDevCon TableName timestamp TargetDb EXISTS"},{"u":"/docs/onboarding/devops-runbooks.html#infradisaster-recoverymd-dr-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/DISASTER-RECOVERY.md, DR runbook","x":"File: MMCA.ADC/infra/DISASTER-RECOVERY.md (175 lines; not the Store file of the same name) What it is. The authoritative disaster-recovery runbook for the ADC production…","i":"publicNetworkAccess scheduledQueryRules serviceDatabaseLtr workflow_dispatch ADC_Notification ADC_Conference ADC_Engagement resourceToken sloAlertSpecs ADC_Identity containerapp keyVaultUrl"},{"u":"/docs/onboarding/devops-runbooks.html#dr-drillyml-and-dr-restore-drillps1-the-adr-009-restore-drill","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"dr-drill.yml and dr-restore-drill.ps1, the ADR-009 restore drill","x":"Files: MMCA.ADC/.github/workflows/dr-drill.yml, MMCA.ADC/scripts/dr-restore-drill.ps1 What it is. The automation behind the drill requirement above: the workflow picks a target…","i":"workflow_dispatch SourceDatabase ADC_Identity deploy.needs deploy.yml AtlDevCon finally restore Online status exit show"},{"u":"/docs/onboarding/devops-runbooks.html#infraoperationsmd-day-2-alert-triage-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/OPERATIONS.md, day-2 alert triage runbook","x":"File: MMCA.ADC/infra/OPERATIONS.md What it is. The alert-to-action companion to the provisioned observability: what to do when each SLO alert fires, how to read the SLO workbook,…","i":"MMCA.Common.Testing.Architecture ObservabilityConventionTestsBase legacySloMetricAlertSpecs infra.OPERATIONS.md MinimumAlertSpecs infra.main.bicep OPERATIONS.md sloAlertSpecs ALERT_EMAIL AppTraces sloAlerts resource"},{"u":"/docs/onboarding/devops-runbooks.html#infrasql-managed-identitymd-staged-passwordless-sql-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/SQL-MANAGED-IDENTITY.md, staged passwordless-SQL runbook","x":"File: MMCA.ADC/infra/SQL-MANAGED-IDENTITY.md What it is. The runbook for moving the four service apps from SQL-login (password) auth to Entra managed-identity auth against their…","i":"vars.USE_MANAGED_IDENTITY_SQL USE_MANAGED_IDENTITY_SQL SQL_AAD_ADMIN_LOGIN SQL_AAD_ADMIN_OID Directory db_owner EXTERNAL Identity PROVIDER Managed Active CREATE"},{"u":"/docs/onboarding/devops-runbooks.html#infrapost-cutover-atldevcon-downgrademd-archive-downgrade-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/POST-CUTOVER-atldevcon-downgrade.md, archive downgrade runbook","x":"File: MMCA.ADC/infra/POST-CUTOVER-atldevcon-downgrade.md What it is. A step-by-step runbook for the third and final commit of the database-per-service rollout: downgrading…","i":"maxSizeBytes ProcessedOn deploy.yml main.bicep AtlDevCon capacity against bacpac update query name NULL"},{"u":"/docs/onboarding/devops-runbooks.html#play-store-captureps1-android-screenshot-capture","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"play-store-capture.ps1, Android screenshot capture","x":"File: MMCA.ADC/scripts/play-store-capture.ps1 What it is. A PowerShell 7 script that captures a screenshot from an attached Android device or emulator via adb screencap and saves…","i":"screencap Files shell PATH slug adb png x86"},{"u":"/docs/onboarding/devops-runbooks.html#play-store-composeps1-play-store-screenshot-compositor","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"play-store-compose.ps1, Play Store screenshot compositor","x":"File: MMCA.ADC/scripts/play-store-compose.ps1 What it is. A PowerShell 7 script that reads raw captures from store-assets/play-store/raw/, wraps each into a 1080×1920 branded…","i":"System.Drawing.Common LinearGradientBrush brandTealDark brandCyan brandTeal imageMaxH imageMaxW slug png"},{"u":"/docs/onboarding/devops-runbooks.html#docsmobilereleaserunbookmd-store-submission-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"Docs/MobileReleaseRunbook.md, store-submission runbook","x":"File: MMCA.ADC/Docs/MobileReleaseRunbook.md What it is. The manual, credential-holding steps around a store submission that code and CI cannot perform, each tagged with when it…","i":"ADC_ANDROID_SIGNING_PASSWORD FileStorage.UploadFailed sha256_cert_fingerprints AndroidSigningStorePass com.ivanball.atldevcon grantAvatarStorageRole AndroidSigningKeyPass deployNotificationHub TargetPlatformVersion InternalServerError Entitlements.plist ivanball.AtlDevCon"},{"u":"/docs/onboarding/devops-runbooks.html#the-database-per-service-cutover-in-full-context","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"The database-per-service cutover in full context","x":"The five database-related artifacts above form a single coherent story, and the resilience artifacts extend it past the cutover: The AtlDevCon database is the thread that runs…","i":"CrossDataSourceDegradeConvention OPERATIONS.md deploy.yml main.bicep AtlDevCon delete NEVER sql"},{"u":"/docs/onboarding/devops-runbooks.html#rubric-tag-summary","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"Rubric tag summary","x":"---","i":"OPERATIONS.md"},{"u":"/docs/onboarding/devops-runbooks.html#not-determinable-from-source","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"Not determinable from source","x":"- ALERTEMAIL variable: DISASTER-RECOVERY.md:55-57 and OPERATIONS.md:8-11 both route alert notifications through the alertEmailAddress action-group receiver fed by the ALERTEMAIL…","i":"alertEmailAddress ALERT_EMAIL"},{"u":"/docs/onboarding/devops-testing.html","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","x":"Chapter scope note. The tier chapters (tier-00 through the sweep) document every type in the production codebase one by one. Test types are the logged exception: this chapter…","i":"Fact"},{"u":"/docs/onboarding/devops-testing.html#1-solution-composition-and-the-test-runner","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"1. Solution composition and the test runner","x":"The two deployed apps use the same two-file pattern; MMCA.Common and MMCA.Helpdesk ship a .slnx only, because their solutions are already fast enough not to need a CI subset:…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests MMCA.ADC.CrossService.IntegrationTests MMCA.ADC.Architecture.Tests MMCA.Store.Integration.slnf MMCA.ADC.Integration.slnf MMCA.ADC.IntegrationTests DistributedCacheService MMCA.ADC.Services.Tests MMCA.ADC.Gateway.Tests MMCA.ADC.WebAPI.Tests MMCA.Common.API.Tests WebApplicationFactory"},{"u":"/docs/onboarding/devops-testing.html#2-test-project-layout","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"2. Test project layout","x":"The inventory below is drawn from 00-inventory.md:23-117 (test-assembly counts) and the solution files above. Counts are distinct types per project as reported by the Roslyn…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests CurrentEventNotificationScopeProviderTests MMCA.ADC.Conference.Infrastructure.Tests MMCA.ADC.Engagement.Infrastructure.Tests MMCA.ADC.Notification.Application.Tests MMCA.ADC.CrossService.IntegrationTests MMCA.ADC.Identity.Infrastructure.Tests MMCA.ADC.Notification.IntegrationTests MMCA.Common.Infrastructure.Redis.Tests NotificationUserDataExportSectionTests MMCA.ADC.Conference.Application.Tests MMCA.ADC.Engagement.Application.Tests"},{"u":"/docs/onboarding/devops-testing.html#3-shipped-testing-infrastructure-packages","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"3. Shipped testing-infrastructure packages","x":"MMCA.Common ships four of its fifteen packages as testing infrastructure that downstream apps consume as NuGet references rather than writing their own harness…","i":"AxeOptions.Wcag21AaExceptMudPagerCombobox WebApplicationFactory.ConfigureServices ServiceInfoVersioningContractTestsBase AssertNoAccessibilityViolationsAsync IsAuthenticatedAuthorizationService SqlServerIntegrationTestFixtureBase MutableAuthenticationStateProvider PageExtensions.FillAndVerifyAsync MMCA.Common.Testing.Architecture ProductionHostApplicationFactory AccessibilityViolationException DecoratorPipelineOrderTestsBase"},{"u":"/docs/onboarding/devops-testing.html#4-architecture-fitness-tests-executable-governance","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"4. Architecture fitness tests, executable governance","x":"[Rubric §34, Architecture Governance & Documentation]: §34 assesses whether architectural decisions are documented, enforced, and kept honest over time; fitness functions are the…","i":"AggregateRoots_ShouldHave_NoPublicConstructors SpecificationsDoNotNavigateToOtherEntities ArchitectureRules.PinnedPackageMajorBelow LayerMap_ModulesDeclareEveryExpectedLayer MassTransit_MustNotExceed_MajorVersion8 CoreLayers_ShouldNotDependOn_Transport ImageSharp_MustNotExceed_MajorVersion3 ObservabilityConventionTestsBaseTests Infrastructure_ShouldNotDependOn_Api ConstructorDependencyCountTestsBase DomainFactories_ShouldReturn_Result FakeDependentModuleConformanceTests"},{"u":"/docs/onboarding/devops-testing.html#5-integration-and-e2e-strategy","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"5. Integration and E2E strategy","x":"The four integration test projects (Identity, Conference, Engagement, Notification) each boot their service in-process with WebApplicationFactory . The lifecycle is not written…","i":"MMCA.Store.ServiceBusEmulator.IntegrationTests MMCA.ADC.ServiceBusEmulator.IntegrationTests MMCA.Store.CrossService.IntegrationTests MMCA.ADC.CrossService.IntegrationTests MMCA.Common.Infrastructure.Redis.Tests AssertNoAccessibilityViolationsAsync IntegrationTestBase.InitializeAsync SqlServerIntegrationTestFixtureBase MMCA.Common.Infrastructure.Tests IdentityIntegrationTestFixture appsettings.Development.json DatabaseInitStrategy.Migrate"},{"u":"/docs/onboarding/devops-testing.html#6-worked-examples","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"6. Worked examples","x":"Three examples tie the infrastructure above to real test code. The per-repo class is a bare subclass; the facts, the package lists and the parsing live once in the shared base:…","i":"IdentityIntegrationTestFixture.DisposeAsync ImageSharp_MustNotExceed_MajorVersion3 IntegrationTestBase.InitializeAsync MutableAuthenticationStateProvider IntegrationTestBase.DisposeAsync IdentityIntegrationTestFixture AuthenticationStateProvider GetAuthenticationStateAsync IdentityIntegrationTestBase Fixture.ResetDatabaseAsync Directory.Packages.props AuthenticateAsAttendee"},{"u":"/docs/onboarding/devops-testing.html#7-the-tiers-and-the-gates-that-run-them","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"7. The tiers and the gates that run them","x":"A test tier only means something once you know what it blocks. This is the map. MMCA.Common's ui-e2e job (MMCA.Common/.github/workflows/ci.yml:228) builds the out-of-slnx gallery…","i":"Integration.slnf MemoryDiagnoser E2E_BROWSER browsers chromium coverage CI.slnf e2e.yml firefox skipped success deploy"},{"u":"/docs/onboarding/devops-testing.html#quick-reference-rubric-categories-touched-in-this-chapter","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"Quick reference: rubric categories touched in this chapter","x":"---"},{"u":"/docs/onboarding/devops-testing.html#cross-links","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"Cross-links","x":"- Primer: 00-primer.md5-the-solution--test-layout , solution files, MTP runner, slnx-excluded UI projects - Primer:…","i":"MMCA.ADC.Integration.slnf IIntegrationTestFixture"},{"u":"/docs/onboarding/99-coverage-audit.html","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","x":"This audit reconciles the written guide against the mechanically-extracted inventory, logs every deliberate exception, verifies the grouping/ordering rules, proves all 34 rubric…","i":"classify.ps1 verify.ps1 plan.ps1"},{"u":"/docs/onboarding/99-coverage-audit.html#1-coverage-reconciliation","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"1. Coverage reconciliation","x":"Cross-check result: verify.ps1 confirms 0 of the 1,890 individually-sectioned types are missing from their group chapter, every one appears as a heading or in a sibling-family…","i":"SpecificationsDoNotNavigateToOtherEntities UserSessionBookmarkCacheEvictionHandler MMCA.Common.Infrastructure.Redis.Tests DatabaseInitializationExtensionsTests HttpContextExternalLoginEmailVerifier MMCA.ADC.Conference.Application.Tests MMCA.ADC.Engagement.Application.Tests ObservabilityConventionTestsBaseTests OwnSessionQuestionAnswerSpecification AnonymousAuthenticationStateProvider AzureNotificationHubNativePushSender CancellationTokenConventionTestsBase"},{"u":"/docs/onboarding/99-coverage-audit.html#2-exceptions-log-every-deliberate-omission-with-reason","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"2. Exceptions log (every deliberate omission, with reason)","x":"EF Core migrations (/Migrations/, .Migrations.SqlServer), ModelSnapshot, .Designer.cs, .g.cs, GlobalUsings.g.cs, and AssemblyInfo.cs are excluded by rule (Tools/invtool…","i":"ObservabilityConventionTestsBase ProductionHostApplicationFactory RouteAuthorizationTestsBase ModuleConformanceTestsBase DependencyInjectionAssert GracefulShutdownTestsBase MMCA.Common.Benchmarks Migrations.SqlServer Testing.Architecture MMCA.Common.Testing GlobalUsings.g.cs AssemblyInfo.cs"},{"u":"/docs/onboarding/99-coverage-audit.html#3-grouping--ordering-verification","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"3. Grouping & ordering verification","x":"- Every type in exactly one group. classify.ps1 assigns all 3,465 nodes via name-level overrides (for the grab-bag MMCA.Common.Interfaces/Services namespaces) + ordered…","i":"MidSaveContextCreatingDbContext OutboxRoutingTestDbContext ReentrantSaveInterceptor FailingSaveInterceptor INavigationPopulator ResultGrpcExtensions EntityQueryService SelfHttpWarmupTask ApiControllerBase DeferredDispatch ErrorHttpMapping _typemap.tsv"},{"u":"/docs/onboarding/99-coverage-audit.html#4-rubric-coverage-matrix","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"4. Rubric coverage matrix","x":"Every one of the 34 categories is explained at least once against real code. \"First explained in\" is the earliest group chapter (by order) that tags it; many recur and several…","i":"ThemeService verify.ps1 token"},{"u":"/docs/onboarding/99-coverage-audit.html#5-open-questions--not-determinable-from-source","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"5. Open questions / not determinable from source","x":"1. IDbSeeder host invocation (group-07). The seeding contract and implementations are in MMCA.Common, but the IHostedService/startup invoker that actually runs seeding at boot…","i":"MMCA.ADC.Identity.Contracts.DependencyInjection ModuleApplicationDbContext CrossSourceSpecification ReadRepositoryExtensions EntityTypeConfiguration DependencyInjection DbContexts.Factory ChangePassword ExportUserData IHostedService EnsureCreated IUnitOfWork"},{"u":"/docs/onboarding/99-coverage-audit.html#6-how-to-regenerate-this-audit","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"6. How to regenerate this audit","x":"Then copy the refreshed out/00-inventory.md and out/00-dependency-manifest.md into Docs/Onboarding/ (the 00-group-taxonomy.md is written there directly by classify.ps1).","i":"classify.ps1"},{"u":"/docs/onboarding/CONCEPT-MAPS.html","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","x":"Mermaid diagrams distilled from the Onboarding guide (primer, group taxonomy, dependency manifest, and the 27 group chapters). Each diagram captures a relationship between the…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#1-system-context-two-codebases--the-15-packages","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"1. System context, two codebases + the 15 packages","x":"MMCA.Common is a framework published as fifteen NuGet packages in lockstep, to nuget.org and GitHub Packages from one tag (ADR-053); MMCA.ADC and MMCA.Store consume them. The…","i":"MMCA.Common.slnx MMCA.Common MMCA.Store MMCA.ADC UI.Maui"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#2-clean-architecture-the-layered-dependency-rule","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"2. Clean Architecture, the layered dependency rule","x":"Source dependencies point inward toward the Domain; each layer references only layers below it. Deliberate exceptions: UI and Grpc depend on Shared only (UI for Blazor WASM…","i":"ProjectReference UI.Maui Aspire Blazor bridge depend Shared above host only sits and"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#3-the-27-functional-groups-dependency--build-order","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"3. The 27 functional groups, dependency / build order","x":"The primary axis of the guide: every type lives in exactly one of 27 chapter groups, ordered roughly topologically. Foundational, widely-depended-on concerns first (Result →…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#4-core-framework-patterns-how-the-building-blocks-compose","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"4. Core framework patterns, how the building blocks compose","x":"The pattern-level view of the same backbone: the ideas the primer commits to and how they feed each other. Result is the pervasive currency; DDD blocks produce domain events;…","i":"Result"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#5-request-lifecycle-the-cqrs-decorator-pipeline-adr-014","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"5. Request lifecycle, the CQRS decorator pipeline (ADR-014)","x":"Handlers are thin (one method); every cross-cutting concern is a decorator wrapping the next. Scrutor TryDecorate composes them in reverse registration order (last registered =…","i":"AddApplicationDecorators TryDecorate"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#6-event-driven-integration-outbox-dual-dispatch-adr-003--010--021","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"6. Event-driven integration, outbox dual-dispatch (ADR-003 / 010 / 021)","x":"Domain events are captured into an OutboxMessage row in the same transaction as the data (no dual-write bug). The two event kinds then part ways: local domain events are…","i":"IIntegrationEventPublisher IEventBus.PublishAsync OutboxProcessor OutboxMessage SchemaVersion IMessageBus MessageId"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#7-modular-monolith--extractable-services-adr-006--007--008--012","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"7. Modular monolith → extractable services (ADR-006 / 007 / 008 / 012)","x":"Modules implement IModule and are discovered + Kahn-ordered by ModuleLoader (ADR-059). The same module code runs as a single monolith host or as N service processes behind a YARP…","i":"MMCA.ADC.WebAPI ModuleLoader IMessageBus IModule"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#8-persistence-database-per-service--polyglot-engines-adr-006--018--030","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"8. Persistence, database-per-service + polyglot engines (ADR-006 / 018 / 030)","x":"One concrete SQLServerDbContext over the abstract ApplicationDbContext, one instance per database. Each entity is engine-agnostic; a single [UseDataSource(engine)] attribute on…","i":"ApplicationDbContext SQLServerDbContext UseDataSource engine"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#9-authentication--authorization-stack","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"9. Authentication & Authorization stack","x":"The auth concern (G08) spans token validation, session cookies, federated sign-in, password hashing, brute-force protection, refresh-token rotation and revocation, and a layered…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#10-notifications-three-channels-behind-one-send-pipeline-adr-024--044","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"10. Notifications, three channels behind one send pipeline (ADR-024 / 044)","x":"One use case (SendPushNotificationHandler) writes a durable per-user inbox, fires a transient SignalR push, and then an OS-level native push that reaches a backgrounded or killed…","i":"SendPushNotificationHandler MMCA.ADC.Notification SmtpEmailSender IEmailSender"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#11-ui-write-once-render-everywhere--i18n--theming","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"11. UI, write-once render everywhere + i18n + theming","x":"A page is authored once as a Razor component in a per-module UI library; both the Blazor web host (Server + WASM) and the .NET MAUI host reference the same libraries, so it…","i":"IStringLocalizer InteractiveAuto MMCA.Common.UI ThemeService rendermode"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#12-adc-business-modules-bounded-contexts-end-to-end","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"12. ADC business modules, bounded contexts end-to-end","x":"Each ADC module is a vertical slice through all layers. Conference is large enough to split across five chapters (G17-G21); Engagement takes two (G22 session bookmarks, G23 the…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#13-the-adrs-grouped-by-theme","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"13. The ADRs, grouped by theme","x":"Every accepted ADR in Website/docs-src/adr/, clustered by the concern it governs. That directory's README.md is the canonical index and owns the count and range; this map only…","i":"README.md"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#14-the-34-category-evaluation-rubric","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"14. The 34-category evaluation rubric","x":"The lens the guide tags code against ([Rubric §N]). Scored on two axes: Maturity (0-4, process) and Implementation (0-10, substance). Three parts. ---","i":"Rubric"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#15-how-the-axes-fit-together-reading-map","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"15. How the axes fit together (reading map)","x":"The guide is organized on two axes at once. This ties the diagrams above back to the guide's navigation. --- - Group-to-group arrows in §3 show the dominant \"builds on\" direction…","i":"ApplicationDbContext"},{"u":"/docs/governance/index.html","d":"Architecture Governance","k":"Architecture Governance","x":"The governance artifacts behind the MMCA platform: the shared 34-category evaluation rubric, and each repo's evidence-based scorecard plus its remediation backlog. Every score…"},{"u":"/docs/governance/index.html#the-rubric","d":"Architecture Governance","k":"Architecture Governance","t":"The rubric","x":"- Architecture Evaluation Criteria: the 34-category rubric (Maturity 0-4 and Implementation 0-10 per category) that all three application repos are scored against."},{"u":"/docs/governance/index.html#how-these-are-maintained","d":"Architecture Governance","k":"Architecture Governance","t":"How these are maintained","x":"Scores are re-verified from source on a cadence: each category is scored by reading the current code, config, and CI (never rolled forward), and any change lands with the…"},{"u":"/docs/governance/adc-ArchitectureScorecard.html","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","x":"Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.ADC's architecture scores (replaces the former single-axis snapshot; see git history).…","i":"dotnet_analyzer_diagnostic.severity SessionSelectionDashboard.razor.cs ArchitectureEvaluationCriteria.md MMCA.ADC.Notification.Application MMCA.Common.Testing.Architecture UIArchitectureConventionTests.cs StateManagementConventionTests SQLitePCLRaw.bundle_e_sqlite3 UIArchitectureConventionTests ObservabilityConventionTests PseudoLocalizationTests RemediationBacklog.md"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#executive-summary","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Executive summary","x":"MMCA.ADC is a mature, evidence-driven .NET 10.0 (LangVersion preview) DDD/Clean Architecture system for the Atlanta Developers Conference, deliberately extracted from a modular…","i":"MMCA.ADC.CrossService.IntegrationTests MMCA.ADC.Notification.IntegrationTests SessionIncludeChildrenRegressionTests UIArchitectureConventionTestsBase FrameworkVersionConsistencyTests LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture ErrorMessages.ValidationError LocalizedTextConventionTests ObservabilityConventionTests SpecificationConventionTests BlazorCspPolicyProvider.cs"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#scorecard","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Scorecard","x":"Weighted column = Maturity·weight / Implementation·weight per row. Axis-gap findings: the former §21 gap (excellent-but-not-enforced, impl 7 over maturity 2) closed on 2026-07-02…","i":"MmcaCultureBootstrap.SetBrowserCultureAsync ResilienceCircuitBreakerFaultInjectionTests MMCA.ADC.CrossService.IntegrationTests dotnet_analyzer_diagnostic.severity FrameworkVersionConsistencyTests.cs StateManagementConventionTestsBase MMCA.ADC.Notification.Application UIArchitectureConventionTestsBase MMCA.Common.Testing.Architecture ConstructorDependencyCountTests LocalizedTextConventionTests.cs ObservabilityConventionTests.cs"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#indices","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Indices","x":"- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 311 ÷ 320 = 97.2% (down from 97.8%, 313/320: the twenty-second-cycle full re-score (2026-07-21, pin v1.121.0) corrected §22…"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#top-5-strengths","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 strengths","x":"1. Architecture intent is executable, not prose: 29 architecture-test classes / 91 methods gate CI (§34 Governance, mat 4 / impl 9, and §3 Clean Architecture, mat 4 / impl 9):…","i":"SessionIncludeChildrenRegressionTests MMCA.Common.Testing.Architecture SpecificationConventionTests.cs AddSessionCookieAuthentication StateManagementConventionTests MicroserviceExtractionTests AddCommonSecurityHeaders ArchitecturalAnalysis.md LayerDependencyTests AddCommonBlazorCsp DataResidencyTests DomainPurityTests"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#top-5-risks","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 risks","x":"Expected-delta note (updated 2026-08-01): several entries below record an expected lift of \"impl 9→10\". Under the 2026-08-01 recalibration those are attainable, not aspirational:…","i":"publicNetworkAccess packages.lock.json MMCA.ADC.CI.slnf deploy.needs maxReplicas MMCA.ADC.UI CI.slnf"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#cross-repo-comparison","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Cross-repo comparison","x":"How this repo's quality relates to the other consumers (where the framework's quality propagates, where a repo diverges or under-uses it, and where a consumer does better than…"},{"u":"/docs/governance/adc-RemediationBacklog.html","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","x":"Derived from ArchitectureScorecard.md (single-axis 0-4, baseline 75%, 241/320, dated 2026-06-08). Current authoritative two-axis scores (twenty-sixth-cycle full re-score,…","i":"MMCA.ADC.CrossService.IntegrationTests SqlServerIntegrationTestFixtureBase SessionSelectionDashboard.razor.cs MMCA.ADC.Notification.Application FrameworkVersionConsistencyTests StateManagementConventionTests UIArchitectureConventionTests IntegrationTestReworkPlan.md LocalizedTextConventionTests ObservabilityConventionTests TranslationCompletenessTests MMCA.ADC.Integration.slnf"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-6-highest-leverage","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔴 Priority 6: highest leverage","x":"Token handling uses two rubric-named anti-patterns, with no CSP defense-in-depth. Status (2026-06-27): cookie-only refresh + in-memory access (auth-path BFF), OAuth…","i":"ResilienceCircuitBreakerFaultInjectionTests DisconnectedCircuitRetentionPeriod ManagementRouteAuthorizationTests GatewaySecurityHeadersMiddleware E2E_LIFT_REGISTRATION_THROTTLE OAuthController.CompleteAsync OAuthController.ExchangeAsync SameOriginProxyTokenRefresher MMCA.ADC.Conference.UI.Tests AuthenticationStateProvider EventDetailPage.StatusChip InvalidOperationException"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-4","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Priority 4","x":"The (4−score)×weight formula puts this at 4, but the High flag is a contractual/regulatory exposure that contradicts a shipped, publicly-served policy: treat it as do-soon. -…","i":"user_notification_export.proto LocalizedTextConventionTests TranslationCompletenessTests user_engagement_export.proto ExportUserDataHandlerTests ErasureAndPiiLoggingTests DeleteUserHandlerTests ErrorMessages.Success SessionQuestionAnswer User.PreferredCulture UserRegisteredHandler EventQuestionAnswer"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-3-score-3-weight-3-one-rung-from-a-4","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟡 Priority 3: score 3, weight 3 (one rung from a 4)","x":"- ~~(High) The 258-test Testcontainers integration tier references the deleted MMCA.ADC.WebAPI host, won't build, and is excluded.~~ RESOLVED: reworked as per-service…","i":"Update_WithStaleRowVersion_ReturnsConflict MMCA.ADC.CrossService.IntegrationTests SessionSelectionDashboard.razor.cs StateManagementConventionTestsBase ManagementRouteAuthorizationTests UIArchitectureConventionTestsBase InProcessEventBus.PublishAsync SessionSelectionSpeakerOverlap StateManagementConventionTests UIArchitectureConventionTests DbUpdateConcurrencyException PublicSessionList.razor.cs"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-2-score-3-weight-2-polish--hardening","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Priority 2: score 3, weight 2 (polish / hardening)","x":"- ~~(Medium) No OpenAPI served by any running service, yet CLAUDE.md still advertises 4 doc UIs (/swagger, /nswag-swagger, /api-docs, /scalar/v1).~~ - [x] Serve OpenAPI per…","i":"Microsoft.AspNetCore.Authorization.Authorize AuthorizationPolicies.RequireOrganizer MMCA.ADC.Conference.IntegrationTests ManagementRouteAuthorizationTests FrameworkVersionConsistencyTests IdentityRouteAuthorizationTests IntegrationEventContractTests MMCA.ADC.Migrations.SqlServer Microsoft.AspNetCore.OpenApi ObservabilityConventionTests MicroserviceExtractionTests Validation.CorrectFollowing"},{"u":"/docs/governance/adc-RemediationBacklog.html#implementation-band-implementation--8-ranked-by-implpriority","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔵 Implementation band (implementation <= 8, ranked by implPriority)","x":"Added 2026-07-28 when the ledger gained its second ranked axis. Until then implementation gaps appeared only as unranked sub-bullets inside maturity items, so they were never…","i":"SessionSelectionDashboard.razor.cs MMCA.ADC.Notification.Application AdcArchitectureMap.DefineLayers ConferenceCategoryDetail.razor HappeningNow.razor.cs TreatWarningsAsErrors DeviceSettings.razor SponsorCreate.razor SponsorDetail.razor System.Private.Uri workflow_dispatch UI.Web.Client"},{"u":"/docs/governance/adc-RemediationBacklog.html#resolved-2026-07-25-performance-program-2","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved 2026-07-25 (performance program 2)","x":"Second evidence-led performance pass over Common/ADC/Store. ADC's share shipped as three PRs plus the v1.127.0 framework sweep. - [x] Output cache shared across replicas.…","i":"Session.SessionQuestionAnswers Event.EventQuestionAnswers SessionQuestionViewBuilder CategoryItemLookupService SessionScoringProcessor SpeakerDashboardService SessionQuestionAnswers EventQuestionAnswers SessionCategoryItems SpeakerCategoryItems GetOpenPollsHandler PublicSessionDetail"},{"u":"/docs/governance/adc-RemediationBacklog.html#deliberate--accepted-recorded-decisions-not-scheduled-work","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"Deliberate / accepted (recorded decisions, not scheduled work)","x":"Conscious, recorded choices, not pending work (the former TECHDEBT.md accepted-risk section): - Single-region deployment (no multi-region failover): accepted in…","i":"SessionRoomScheduling.ValidateRoomAssignmentAsync MMCA.ADC.Notification.Application ConstructorDependencyCountTests LocalizedTextConventionTests TranslationCompletenessTests ArchitecturalAnalysis.md PseudoLocalizationTests AuthenticationService BrandColorTokenTests DeviceSettings.razor skip_freshness_gates SliceCohesionTests"},{"u":"/docs/governance/adc-RemediationBacklog.html#already-at-level-4-protect-dont-regress","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"✅ Already at level 4: protect, don't regress","x":"1 SOLID · 2 Design Patterns · 3 Clean Architecture · 4 Domain-Driven Design · 5 Vertical Slice Architecture · 6 CQRS & Event-Driven · 7 Microservices Readiness · 8 Data…","i":"AnthropicScoringService.ScoreSessionAsync MMCA.ADC.CrossService.IntegrationTests GetSessionSelectionDashboardHandler SessionSelectionDashboard.razor.cs GetSpeakerSessionOverlapHandler GetCategoryDistributionHandler Session.AddSessionCategoryItem Session.CategoryItem.Duplicate Speaker.AddSpeakerCategoryItem Speaker.CategoryItem.Duplicate ObservabilityConventionTests OperationCanceledException"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html","d":"Architecture Evaluation Criteria","k":"Architecture Governance","x":"A structured rubric for evaluating the architecture of an enterprise application. Each category defines what is being assessed, concrete criteria to check, red flags that signal…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#how-to-use-this-rubric","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"How to Use This Rubric","x":"Score each category 0–4. Use the same scale everywhere so totals are comparable. Alongside the maturity level, rate how well each category is actually implemented on a finer 0–10…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#1-solid-principles","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"1. SOLID Principles","x":"Intent: Object/module-level design discipline that keeps code flexible and decoupled. Criteria - SRP: each class/handler has one reason to change; no \"god\" services orchestrating…","i":"NotSupportedException switch new"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#2-design-patterns","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"2. Design Patterns","x":"Intent: Appropriate, idiomatic use of patterns, solving real problems, not pattern theater. Criteria - Creational (Factory methods on entities, Builder, Options) used where…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#3-clean-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"3. Clean Architecture","x":"Intent: Dependencies point inward; business rules are independent of frameworks, UI, and data stores. Criteria - Dependency rule enforced: Domain → (nothing); Application →…","i":"JsonProperty DbContext Table"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#4-domain-driven-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"4. Domain-Driven Design","x":"Intent: The model reflects the business; boundaries follow capability boundaries, not technical layers. Criteria - Ubiquitous language: type/method names match business terms…","i":"decimal Result string Guid"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#5-vertical-slice-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"5. Vertical Slice Architecture","x":"Intent: Code is organized by feature/capability, so a change touches one cohesive slice. Criteria - Features grouped by use case (command/query + handler + validator + DTO…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#6-cqrs--event-driven-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"6. CQRS & Event-Driven Design","x":"Intent: Reads and writes are separated where it pays off; integration via events is reliable. Criteria - Commands (mutate, return Result) and queries (read, side-effect-free) are…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#7-microservices-readiness","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"7. Microservices Readiness","x":"Intent: Whether services (or future-extractable modules) are independently deployable and own their data. Criteria - Service boundaries align with bounded contexts; one team can…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#8-data-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"8. Data Architecture","x":"Intent: Persistence, consistency, and migrations are deliberate and safe. Criteria - Transaction boundaries match aggregate boundaries; unit-of-work scope is clear. - Migrations…","i":"Include"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#9-api--contract-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"9. API & Contract Design","x":"Intent: External and inter-service contracts are clear, stable, and evolvable. Criteria - Consistent resource/endpoint design (REST/minimal APIs/gRPC) with predictable shapes. -…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#10-cross-cutting-concerns","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"10. Cross-Cutting Concerns","x":"Intent: Validation, caching, resilience, configuration, and mapping are centralized and consistent. Criteria - Validation, logging, transactions handled by pipeline…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#11-security","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"11. Security","x":"Intent: AuthN/AuthZ, secrets, and data protection are correct by construction. Criteria - Authentication centralized; tokens validated; identity flows documented (e.g.,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#12-performance--scalability","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"12. Performance & Scalability","x":"Intent: The system meets latency/throughput goals and scales horizontally. Criteria - Async I/O throughout; no sync-over-async; no blocking the request thread. - Hot-path query…","i":"Result Wait"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#13-observability--operability","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"13. Observability & Operability","x":"Intent: You can understand and operate the system in production. Criteria - Structured logging with correlation/trace IDs flowing across module/service boundaries. - Distributed…","i":"Console.WriteLine"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#14-testability--test-strategy","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"14. Testability & Test Strategy","x":"Intent: The design supports fast, reliable, meaningful tests at the right levels. Criteria - Healthy test pyramid: many fast unit tests on domain/application, fewer integration,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#15-best-practices--code-quality","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"15. Best Practices & Code Quality","x":"Intent: Day-to-day craftsmanship that keeps the codebase healthy. Criteria - Analyzers at error severity (style, security, threading, maintainability) enforced in CI;…","i":"disable warning pragma"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#16-maintainability--evolvability","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"16. Maintainability & Evolvability","x":"Intent: The system absorbs change cheaply and ages well. (The governance/documentation depth behind this (ADRs, fitness functions, diagrams) is scored separately in §34.)…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#17-devops--deployment","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"17. DevOps & Deployment","x":"Intent: Building, releasing, and provisioning are automated, repeatable, and safe. (The local developer experience / inner loop behind this (local orchestration, cross-repo dev,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#18-ui-architecture--component-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"18. UI Architecture & Component Design","x":"Intent: Components are cohesive, reusable, and composed cleanly, the UI has a deliberate structure, not page-sized blobs. Criteria - Container/presentational split: smart…","i":"EventCallback ShouldRender razor key"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#19-state-management--data-flow","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"19. State Management & Data Flow","x":"Intent: Client state has a clear owner and predictable flow; server state is cached and invalidated deliberately. Criteria - Single source of truth per piece of state; ownership…","i":"StateHasChanged IsDirty"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#20-design-system-theming--ui-consistency","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"20. Design System, Theming & UI Consistency","x":"Intent: A coherent visual language enforced by a component library, not re-implemented per screen. Criteria - Component library used consistently (e.g., MudBlazor): teams build…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#21-accessibility-a11y","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"21. Accessibility (a11y)","x":"Intent: The UI is usable by everyone, including assistive-technology users, and ideally enforced, not aspirational. Criteria - Semantic structure: correct…","i":"span div"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#22-responsive-design--cross-browserdevice","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"22. Responsive Design & Cross-Browser/Device","x":"Intent: The UI works across viewport sizes, input modes, and supported browsers. Criteria - Fluid/responsive layouts via the design system's grid/breakpoints; no fixed-width…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#23-front-end-performance--rendering","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"23. Front-End Performance & Rendering","x":"Intent: The UI loads and responds fast; rendering work is bounded. (Complements §12: this is the client side.) Criteria - Initial load: bundle/payload size controlled;…","i":"ShouldRender key"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#24-forms-validation--ux-safety","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"24. Forms, Validation & UX Safety","x":"Intent: Data entry is safe, forgiving, and consistent, users don't lose work or get confused by errors. Criteria - Validation parity: client-side validation for fast feedback…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#25-navigation-routing--information-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"25. Navigation, Routing & Information Architecture","x":"Intent: Users can find their way; routes are meaningful, guarded, and role-aware. Criteria - Route design: clean, bookmarkable, deep-linkable URLs; parameters typed and…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#26-front-end-security","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"26. Front-End Security","x":"Intent: The client doesn't become the weak link, XSS, token handling, and trust boundaries are correct. (Complements §11.) Criteria - Output encoding / XSS: no unsanitized HTML…","i":"MarkupString innerHTML"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#27-internationalization--localization","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"27. Internationalization & Localization","x":"Intent: The UI can be translated and respects culture, if in scope. (Score weight 0–1 if single-locale by design.) Criteria - Externalized strings: UI text in resource files, not…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#28-front-end-testing--quality","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"28. Front-End Testing & Quality","x":"Intent: The UI is verified at the right levels with stable, meaningful tests. (Complements §14.) Criteria - Component tests (e.g., bUnit) for rendering logic, parameters, events,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#29-resilience-reliability--business-continuity","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"29. Resilience, Reliability & Business Continuity","x":"Intent: The system survives partial failure and recovers from disaster within defined objectives. (Extends the resilience facets of §7/§12 into a first-class recovery story.)…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#30-compliance-privacy--data-governance","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"30. Compliance, Privacy & Data Governance","x":"Intent: Personal and regulated data is classified, governed, and handled lawfully across its lifecycle. (§11 defends against attackers; this answers to regulators.) Criteria -…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#31-cost-efficiency--finops","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"31. Cost Efficiency / FinOps","x":"Intent: Cloud spend is proportional to value and driven by data, not guesswork. (§17 mentions cost; this makes it a first-class axis.) Criteria - Right-sizing: compute/database…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#32-dependency--supply-chain-management","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"32. Dependency & Supply-Chain Management","x":"Intent: Third-party and inter-package dependencies are controlled, auditable, and evolve safely, especially critical for a framework that publishes packages. (Elevates §15's…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#33-developer-experience--inner-loop","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"33. Developer Experience & Inner Loop","x":"Intent: Developers build, run, test, and iterate locally with fast, low-friction feedback. (Promoted out of §17: that scores release/ops automation; this scores the inner loop.)…","i":"editorconfig local.props"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#34-architecture-governance--documentation","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"34. Architecture Governance & Documentation","x":"Intent: Decisions are recorded, conformance is enforced, and the system is documented so it stays coherent as it evolves. (Promoted out of §16: that scores the property of…","i":"CLAUDE.md"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#appendix-quick-scan-checklist","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"Appendix: Quick-Scan Checklist","x":"A 2-minute triage before the full evaluation: any \"no\" warrants a deeper look. - [ ] Can you draw the dependency graph and is it acyclic and inward-pointing? - [ ] Is the domain…"},{"u":"/docs/governance/common-ArchitectureScorecard.html","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","x":"Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.Common's architecture scores (replaces the former single-axis snapshot; see git…","i":"ResilienceCircuitBreakerFaultInjectionTests SpecificationsDoNotNavigateToOtherEntities CrossSourceSpecification.BuildAsync BaseIntegrationEvent.SchemaVersion AggregateRootEntityControllerBase ArchitectureEvaluationCriteria.md DomainInvariantViolationException LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture OpenIdConnectMetadataWarmupTask ResourceTranslationsAreComplete EventVersioningConventionTests"},{"u":"/docs/governance/common-ArchitectureScorecard.html#scorecard","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Scorecard","x":"Weighted column = Maturity·weight / Implementation·weight per row. Axis-gap findings: §17/§8 are mature-but-execution-deferred (mechanism shipped, deeper proof lives downstream);…","i":"SpecificationsDoNotNavigateToOtherEntities CrossSourceSpecification.BuildAsync BaseIntegrationEvent.SchemaVersion TransportDoesNotLeakIntoCoreLayers ArchitectureEvaluationCriteria.md CrossDataSourceDegradeConvention MMCA.Common.Testing.Architecture OpenIdConnectMetadataWarmupTask EventVersioningConventionTests ListPageQueryStateServiceTests PermissionAuthorizationHandler PiiErasureContractFitnessTests"},{"u":"/docs/governance/common-ArchitectureScorecard.html#indices","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Indices","x":"- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 316 ÷ 324 = 97.5% (up from 96.9% (314/324) on the targeted 2026-08-22 update: §9 API & Contract Design Maturity 3→4 on…","i":"ServiceContract"},{"u":"/docs/governance/common-ArchitectureScorecard.html#top-5-strengths","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 strengths","x":"1. Dual-enforced Clean Architecture dependency rule (compile-time + fitness functions), §3 (impl 9): Source/Build/MMCA.Common.LayerEnforcement.targets:1-90 fails the build on…","i":"BaseIntegrationEvent.SchemaVersion MMCA.Common.Testing.Architecture EventVersioningConventionTests ResolveProjectReferences packages.lock.json FixedTimeEquals Result.Failure BeforeTargets Theory Fact"},{"u":"/docs/governance/common-ArchitectureScorecard.html#top-5-risks","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 risks","x":"Note (twenty-first wave, v1.121.0): earlier waves closed risks previously listed here (§29's restore drill, the §27 i18n train, §24 forms enforcement, §22's firefox gate, §23's…","i":"PiiErasureContractFitnessTests ServiceContractPurityTestsBase OutboxPollFilterProcessor NavigationContractTests required_status_checks PiiConventionTests CONTRIBUTING.md ServiceContract DEPLOYMENT.md IAnonymizable PiiRedactor COST.md"},{"u":"/docs/governance/common-ArchitectureScorecard.html#cross-repo-comparison","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Cross-repo comparison","x":"How this repo's quality relates to the other consumers (where the framework's quality propagates, where a repo diverges or under-uses it, and where a consumer does better than…"},{"u":"/docs/governance/common-RemediationBacklog.html","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","x":"Derived from ArchitectureScorecard.md (canonical two-axis scoring: Maturity 97.5% / Implementation 84.8%, framework v1.152.0. Targeted update 2026-08-22: 9 API & Contract Design…","i":"ArchitectureScorecard.md required_status_checks RedisDistributedLock IDistributedLock BenchmarkDotNet IsDirtyAccessor ServiceContract Performance baseline c911480 f292233 NoWarn"},{"u":"/docs/governance/common-RemediationBacklog.html#implementation-band-implementation--8-ranked-by-implpriority","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Implementation band (implementation <= 8, ranked by implPriority)","x":"Added 2026-07-28 when the ledger gained its second ranked axis. Until then implementation gaps were never ranked or scheduled, which is why consecutive steady-state cycles moved…","i":"ArchitecturalAnalysis.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-first-wave-2026-06-08","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: first wave (2026-06-08)","x":"Implemented in MMCA.Common, ✅ verified 2026-06-09: dotnet build -c Release is clean (0 warnings / 0 errors, all analyzers) and all 9 test projects pass (~1,611 tests, 0…","i":"MessageBusSettings.RetryLimit ConfigureBrokerTransport Directory.Packages.props IntegrationEventConsumer RetryMaxIntervalSeconds RetryMinIntervalSeconds DependencyVersionTests OutboxCleanupService UseMessageRetry MobileCardList BunitTestBase IAnonymizable"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-second-wave-2026-06-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: second wave (2026-06-09)","x":"✅ Verified: dotnet build -c Release clean (0/0) and all 9 test projects pass (1,511 tests, 0 failures). - ✅ 32 / 16: supply-chain. NuGet lock files (RestorePackagesWithLockFile,…","i":"RestorePackagesWithLockFile ServiceContractAttribute nuget.config CqrsMetrics WithMetrics AddMeter package Release dotnet snupkg build list"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-third-wave-front-end-2026-06-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: third wave (front-end, 2026-06-09)","x":"✅ Verified: build clean (0/0) and all 9 test projects pass (1,519 tests, 0 failures); UI tests 90 → 98 (8 new bUnit tests). - ✅ 19: UnsavedChangesGuard live-accessor. Added…","i":"Page.AssertNoAccessibilityViolationsAsync Deque.AxeCore.Playwright MobileInfiniteScrollList UnsavedChangesGuard MaxRenderedItems IsDirtyAccessor CurrentIsDirty PageLoading PageHeader Virtualize MMCATheme PageError"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-fourth-wave-breaking-changes--consumer-sweep-2026-06-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: fourth wave (breaking changes + consumer sweep, 2026-06-09)","x":"✅ Verified across all three repos (built/tested via local.props against Common source, no token): Common 1,523, ADC 1,241, Store 1,088 tests, 0 failures; all CI solutions build…","i":"AggregateConventionTests IntegrationEventConsumer UserNotification.Create EntityConventionTests OutboxCleanupService AddInboxMessages UserNotification BaseDomainEvent NoOpInboxStore InboxMessages EfInboxStore IDomainEvent"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1800-2026-06-26","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: v1.80.0 (2026-06-26)","x":"The single-axis backlog above is from the 2026-06-08/09 review (index 80%). The framework has since reached v1.82.0 and the canonical scoring was the in-repo, two-axis…","i":"PermissionAuthorizationHandler BaseDomainEvent.DateOccurred UserNotification.MarkAsRead PermissionRegistryBuilder AddAuthorizationPolicies ArchitectureScorecard.md GlobalRateLimitPartition PermissionPolicyProvider RateLimitPartitionTests RoleNames.ContentEditor UserNotification.ReadOn IPermissionRegistry"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1810v1820--governance-pass-2026-06-26","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: v1.81.0/v1.82.0 + governance pass (2026-06-26)","x":"Released since v1.80.0 (v1.81.0, v1.82.0) plus a sixth governance pass currently in flight (uncommitted). All of it lands in categories already scored 9-10, so the two-axis…","i":"ArchitectureEvaluationCriteria.md MMCA.Common.Aspire.Security SecurityHeadersMiddleware AddCommonSecurityHeaders ICspPolicyProvider MapCommonScalarUi Scalar.AspNetCore ValidAlgorithms RsaSha256 FACTS.md b9a6a28 COST.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1830v1840-2026-06-27","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: v1.83.0/v1.84.0 (2026-06-27)","x":"Released since v1.82.0 (v1.83.0, v1.84.0) plus a docs-only governance pass currently in flight (uncommitted). One score moved at this wave: §30 Implementation 7→8. The canonical…","i":"OpenIdConnectMetadataWarmupTask INotificationRecipientProvider ArchitectureScorecard.md IPushNotificationSender WarmupHostedService WarmupReadinessGate AddServiceDefaults PiiConventionTests PiiRedactorTests UserNotification IWarmupTask PiiRedactor"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1850-eighth-wave-under-8-implementation-remediation-2026-06-27","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress, v1.85.0 (eighth wave: under-8 Implementation remediation, 2026-06-27)","x":"The under-8 Implementation remediation (commit 78e5312, tag v1.85.0, HEAD 7082a5f) lifted every category scored Implementation one maturity score. Re-verified against current…","i":"MMCA.Common.Testing.Architecture ArchitectureRules.Slices.cs PasswordComplexityAttribute ArchitectureScorecard.md AuthModelValidationTests DataAnnotationsValidator ServiceContractAttribute TraceIdRatioBasedSampler SliceCohesionTestsBase ParentBasedSampler SliceCohesionTests NavigationFlow.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1860v1920-ninth-wave-i18n--re-score-2026-06-29","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress, v1.86.0→v1.92.0 (ninth wave: i18n + re-score, 2026-06-29)","x":"Re-scored against current source at framework v1.92.0 (HEAD 93ffcac, dirty tree). Canonical scoring is now Maturity 91.7% / Implementation 84.1% (was 92.8% / 85.0%) per the…","i":"PiiErasureContractFitnessTests WebApplicationExtensions.cs ArchitectureScorecard.md ConfigureBrokerTransport IntegrationEventConsumer User.PreferredCulture UseDelayedRedelivery cfg.UseMessageRetry PiiConventionTests DataSubjectSample PasswordHasher.cs IStringLocalizer"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-tenth-wave-focused-in-repo-remediation-2026-06-30","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: tenth wave (focused in-repo remediation, 2026-06-30)","x":"Four scores moved up on shipped, tested in-repo evidence; both indices rose for the first time in several waves: Maturity 91.7% → 92.9% (301/324), Implementation 84.1% → 84.9%…","i":"MMCA.Common.Testing.Architecture PaletteDark.PrimaryContrastText ResourceTranslationsAreComplete DatabaseRestoreDrillTests LocalizationResourceTests Directory.Packages.props PrimitivesSnapshotTests SupportedCultures.All PaletteDark.Primary WarningContrastText ErrorContrastText ACCESSIBILITY.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-eleventh-wave-adr-governance-2026-06-30","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: eleventh wave (ADR governance, 2026-06-30)","x":"No score moves. A full 34-category evidence re-score at framework v1.93.0 (HEAD 3e72bfa, dirty tree) re-confirmed every category at its tenth-wave value; indices hold at Maturity…","i":"AggregateRootEntityControllerBase EntityControllerBase OwnerOrAdminFilter OwnershipHelper Specification customer_id FACTS.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-twelfth-wave-under-8-implementation-lift-v1940-pending-2026-06-30","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: twelfth wave (under-8 Implementation lift, v1.94.0 pending, 2026-06-30)","x":"Two Implementation scores move up, Maturity holds: Implementation 84.9% → 85.3% (691/810), Maturity 92.9% (301/324) unchanged. Full Release build clean, 1685 tests pass. Held for…","i":"LocalizedTextConventionTestsBase ListPageQueryStateServiceTests SupportedCultures.PseudoLocale LocalizedTextConventionTests PseudoStringLocalizerFactory UseCommonRequestLocalization PseudoLocalizationE2ETests ListPageStateServiceTests LocalizationResourceTests PseudoLocalizer.Transform IStringLocalizerFactory PseudoLocalizationTests"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---fourteenth-wave-clean-tree-evidence-re-score-at-v11010-2026-07-03","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - fourteenth wave (clean-tree evidence re-score at v1.101.0, 2026-07-03)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.101.0 (HEAD 5e55be2, working tree clean: the recurring…","i":"ArchitectureScorecard.md FormsConventionTestsBase RegisterFormTests.cs Testing.Architecture Scalar.AspNetCore ValidationMessage FACTS.md slnx"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---defect-fix-wave-c-1c-7-2026-07-05","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - defect-fix wave C-1..C-7 (2026-07-05)","x":"Seven approved defect fixes, each behavior change landed with its pinning test flipped (or a new regression test) in the same change; build 0/0 and the full .slnx suite green.…","i":"Microsoft.Extensions.TimeProvider.Testing EntityServiceBase.GetAllForLookupAsync SessionCookieAuthenticationHandler OAuthControllerBase.CompleteAsync AuthenticatedServiceBase ChildEntityServiceBase LoginProtectionService LoggingQueryDecorator ITokenStorageService KeyNotFoundException OutboxCleanupService Uri.EscapeDataString"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-sixteenth-wave-clean-tree-re-score-at-v11060-2026-07-06","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: sixteenth wave (clean-tree re-score at v1.106.0, 2026-07-06)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.106.0 (HEAD 6f8b917, one commit past the v1.106.0 tag, working tree…","i":"ArchitecturalAnalysis.md ArchitectureScorecard.md Directory.Packages.props EncryptedStringConverter SECURITY.md FACTS.md b75fa8f Theory Fact"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---seventeenth-wave-evidence-re-score-at-v11080-2026-07-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - seventeenth wave (evidence re-score at v1.108.0, 2026-07-09)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.108.0 (git HEAD 6c3b3bc, working tree clean, one commit ahead of…","i":"ILiveChannelPublisher ACCESSIBILITY.md FACTS.md ci.yml"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---eighteenth-wave-runtime-performance-wave-2026-07-10","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - eighteenth wave (runtime performance wave, 2026-07-10)","x":"A cross-repo runtime-performance audit (4 parallel auditors: framework, ADC, Store, hosting/config) found the framework strong on read-path fundamentals (no-tracking, SQL…","i":"PublicEndpointOutputCachePolicy EFReadRepository.ApplyIncludes PooledConnectionLifetime HttpResilienceDefaults CachingQueryDecorator LocalView.FindEntry ExecuteUpdateAsync InProcessEventBus AllowAnonymous DetectChanges ExpandoObject CHANGELOG.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---remediation-wave-1-cross-repo-wave-plan-2026-07-11","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - remediation wave 1 (cross-repo wave plan, 2026-07-11)","x":"First wave of the 2026-07-11 cross-repo remediation plan (workspace plan file). Ships the shared §18/§19 fitness bases the ADC/Store maturity lifts need, closes the tenth-wave 20…","i":"StateManagementConventionTestsBase UIArchitectureConventionTestsBase ErrorMessages._localizer MobileInfiniteScrollList AllowedStaticMembers PrimaryContrastText WebVitalsCollector ErrorContrastText WebVitalsE2ETests DarkModeE2ETests NotificationBell CHANGELOG.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---eighteenth-wave-evidence-re-score-at-v11150-2026-07-12","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - eighteenth wave (evidence re-score at v1.115.0, 2026-07-12)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.115.0 (HEAD 37d0a3b, working tree clean, at the release tag). Three…","i":"ArchitectureScorecard.md MMCA.Common.UI.Maui PrimaryContrastText ErrorContrastText WebVitalsE2ETests DarkModeE2ETests MudDataGrid FACTS.md rgba"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twentieth-wave-evidence-re-score-at-v11170-2026-07-17","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twentieth wave (evidence re-score at v1.117.0, 2026-07-17)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.117.0 (HEAD 76d70cf, working tree clean). Four scores move.…","i":"ArchitectureScorecard.md NavigationContractTests required_status_checks AuthorizeAttribute NavigationFlow.md MMCA.Common.UI RouteAttribute RESPONSIVE.md FACTS.md bicep build Short"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-first-wave-evidence-re-score-at-v11210-2026-07-21","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-first wave (evidence re-score at v1.121.0, 2026-07-21)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.121.0 (HEAD 4a4fc05, working tree clean). One score moves.…","i":"ArchitectureScorecard.md required_status_checks BenchmarkDotNet CONTRIBUTING.md Notifications Performance baseline FACTS.md COST.md verify Short gate"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-second-wave-evidence-re-score-at-v11230-2026-07-23","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-second wave (evidence re-score at v1.123.0, 2026-07-23)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.123.0 (HEAD c911480, working tree clean). No score moves. Canonical…","i":"UnsavedChangesGuard.IsDirtyAccessor PiiErasureContractFitnessTests PasswordComplexityAttribute IIntegrationEventPublisher ArchitectureScorecard.md OpenApiContractTestsBase IConnectionMultiplexer EntityQueryPipeline IEventBus EditForm FACTS.md c911480"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-third-wave-evidence-re-score-at-v11280-2026-07-25","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-third wave (evidence re-score at v1.128.0, 2026-07-25)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.128.0 (HEAD 3dff29b, working tree clean). No score moves, the third…","i":"ArchitectureScorecard.md WebVitalsE2ETests ICommandHandler IQueryHandler pull_request permissions Unreleased FACTS.md TResult ci.yml github Result"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-fourth-wave-evidence-re-score-at-v11310-2026-07-28","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-fourth wave (evidence re-score at v1.131.0, 2026-07-28)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.131.0 (HEAD 2c52aa9, working tree clean). No score moves, the…","i":"ArchitectureScorecard.md OpenApiContractTestsBase AddCommonApiVersioning MMCA.Common.UI.Maui ICommandHandler ServiceContract AllowAnonymous AllowAnyOrigin IQueryHandler FACTS.md TResult CS1591"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-fifth-wave-evidence-re-score-at-v11350-2026-08-01","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-fifth wave (evidence re-score at v1.135.0, 2026-08-01)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.135.0 (HEAD f292233, working tree clean). One score moves, ending…","i":"EntityQueryService.GetAllForLookupAsync DomainInvariantViolationException ArchitectureScorecard.md InProcessDistributedLock HttpResilienceDefaults IConnectionMultiplexer RedisDistributedLock NuGetAuditSuppress IdempotencyFilter IDistributedLock v1.128.0..HEAD AddCaching"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-sixth-wave-evidence-re-score-at-v11420-2026-08-07","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-sixth wave (evidence re-score at v1.142.0, 2026-08-07)","x":"Full 34-category two-pass re-score at HEAD 710d29d (clean tree). No scores move: 27 categories re-confirmed fresh, and seven first-pass lift proposals were refuted on the…","i":"GetAllForLookupAsync packages.lock.json AddMeter FACTS.md orderBy OrderBy secrets l.Name navbar NoWarn where"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---security-invariants-wave-11-hardening-2026-08-22","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - security invariants wave (§11 hardening, 2026-08-22)","x":"Closes the two §11 gaps surfaced by the Article 16 (JWKS dual-fetch) review: the insecure dev defaults that no two-axis entry named as scheduled work, and the absent security…","i":"AnonymousEndpointTestsBase AddForwardedJwtBearer RequireHttpsMetadata RsaJwksProvider AllowAnonymous false main true"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---9-contract-surface-gates-2026-08-22","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - §9 contract-surface gates (2026-08-22)","x":"Closes both halves of 9, the last weight-2 Maturity-3 item that had a named in-repo lever. Landed via MMCA.Common PR 271 (squash 8a6c603, merged 2026-08-22). - ✅ OpenAPI…","i":"ServiceContractsDoNotDependOnServiceInternals ServiceContractPurityTestsBase OpenApiBaselineTests AddCommonOpenApi MapCommonOpenApi ServiceContract ProblemDetails FACTS.md"},{"u":"/docs/governance/common-RemediationBacklog.html#deferred---2026-07-19-full-review-recorded-not-scheduled","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Deferred - 2026-07-19 full review (recorded, not scheduled)","x":"The 2026-07-19 full framework review shipped its accepted fixes on the review branch (rollback on business failure + post-commit dispatch, outbox leases + dead-letter visibility,…","i":"MMCA.Common.Infrastructure MMCA.Common.UI.Tests MMCA.Common.UI.Maui IServiceCollection IMessageBus LangVersion extension IsFailure preview TResult CS1591 NoWarn"},{"u":"/docs/governance/common-RemediationBacklog.html#recorded---2026-07-31-consumer-discovered-defect-not-scheduled","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Recorded - 2026-07-31 consumer-discovered defect (not scheduled)","x":"Found downstream while implementing MMCA.ADC BR-239 (public speaker visibility), which needed a filtered lookup read. Recorded rather than fixed in place: the consumer already…","i":"EntityQueryService.GetAllForLookupAsync MMCA.Common.Shared.ValueObjects.Email IRepository.GetAllForLookupAsync QueryFieldService.Validate InvalidOperationException GetOrBuildLookupSelector BaseLookup.Name nameProperty asTracking ToString orderBy OrderBy"},{"u":"/docs/governance/common-RemediationBacklog.html#priority-6-highest-leverage","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔴 Priority 6: highest leverage","x":"The package ships reusable Blazor primitives with no fast test tier. - ~~(medium) No component tests for the UI library~~ RESOLVED: Tests/Presentation/MMCA.Common.UI.Tests…","i":"Page.AssertNoAccessibilityViolationsAsync PiiErasureContractFitnessTests AuditableBaseEntity.Delete Deque.AxeCore.Playwright EncryptedStringConverter MobileInfiniteScrollList MMCA.Common.Testing.E2E MMCA.Common.Testing.UI MMCA.Common.UI.Tests OutboxCleanupService UnsavedChangesGuard DeleteConfirmation"},{"u":"/docs/governance/common-RemediationBacklog.html#priority-3-score-3-weight-3-one-rung-from-a-4","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Priority 3: score 3, weight 3 (one rung from a 4)","x":"- ~~(medium) No broker retry policy on the extracted-microservice path~~ RESOLVED (re-verified 2026-06-29): ConfigureBrokerTransport applies cfg.UseMessageRetry (exponential) on…","i":"DomainAggregateRootsHaveNoPublicConstructors ResilienceCircuitBreakerFaultInjectionTests Add_DifferentCurrencies_ReturnsFailure HandleBeforeInternalNavigationAsync MobileInfiniteScrollListTests.cs AggregateRootsHaveResultFactory MessageBusSettings.RetryLimit AggregateConventionTestsBase DomainExposesAggregateRoots DomainFactoriesReturnResult RestorePackagesWithLockFile UnsavedChangesGuardTests.cs"},{"u":"/docs/governance/common-RemediationBacklog.html#priority-2-score-3-weight-2-polish--hardening","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟡 Priority 2: score 3, weight 2 (polish / hardening)","x":"- (medium) No consumer-side idempotency/inbox for at-least-once broker delivery: duplicate side effects possible in any non-idempotent consumer. (low) ~~Same misleading…","i":"ServiceContractsDoNotDependOnServiceInternals EntityQueryPipeline.MaxUnboundedResultLimit ApplicationSettings.MaxPageSize MessageBusSettings.EnableInbox ServiceContractPurityTestsBase ArchitectureRules.Slices.cs MobileInfiniteScrollList OpenApiContractTestsBase ServiceContractAttribute Directory.Build.targets AddCommonApiVersioning required_status_checks"},{"u":"/docs/governance/common-RemediationBacklog.html#already-at-level-4-protect-dont-regress","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"✅ Already at level 4: protect, don't regress","x":"1 SOLID · 2 Design Patterns · 3 Clean Architecture · 5 Vertical Slice (maturity 3→4 on the slice-cohesion fitness function) · 7 Microservices Readiness · 8 Data Architecture · 10…","i":"MessageBusSettings.EnableInbox NavigationContractTests IConnectionMultiplexer required_status_checks WebVitalsE2ETests IDistributedLock BenchmarkDotNet ServiceContract EditorRequired Performance baseline navbar"},{"u":"/docs/governance/common-RemediationBacklog.html#deliberate--accepted-documented-caps-not-scheduled-work","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔒 Deliberate / accepted (documented caps, not scheduled work)","x":"Moved out of the active priority queue on 2026-07-02 (user-approved). Its computed priority = (4 − 2) × 2 = 4 is the highest weighted gap of any open category, but the unmet §31…","i":"NavigationFlow.md ACCESSIBILITY.md CONTRIBUTING.md NUGET_API_KEY RESILIENCE.md RESPONSIVE.md CHANGELOG.md release.yml SECURITY.md main.bicep CLAUDE.md README.md"},{"u":"/docs/governance/common-RemediationBacklog.html#mostly-consumer-assessed-the-shared-commonui-surface-is-scored-here","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"⚪ Mostly consumer-assessed (the shared Common.UI surface is scored here)","x":"21 Accessibility · 26 Front-End Security (Assessable mainly in consumer apps; 26 shared surface is covered under 11.) - 22 Responsive: CLOSED at Maturity 4 / Implementation 9…","i":"LocalizedTextConventionTests PseudoLocalizationE2ETests AuthModelValidationTests NavigationContractTests PasswordComplexity NavigationFlow.md RegisterFormTests ValidationMessage ResxMudLocalizer Forbidden EditForm slnx"},{"u":"/docs/governance/store-ArchitectureScorecard.html","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","x":"Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.Store's architecture scores. This is Store's first in-repo governance artifact…","i":"CK_InventoryItem_AvailableQuantity_NonNegative ArchitectureEvaluationCriteria.md ConstructorDependencyCountTests StateManagementConventionTests UIArchitectureConventionTests ObservabilityConventionTests MobileInfiniteScrollList PseudoLocalizationTests GracefulShutdownTests Money.ToDisplayString RemediationBacklog.md ServiceInfoController"},{"u":"/docs/governance/store-ArchitectureScorecard.html#executive-summary","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Executive summary","x":"MMCA.Store is a .NET 10.0 (LangVersion preview) DDD/Clean Architecture e-commerce system (Catalog, Sales, Identity modules; Stripe checkout) extracted into independently-hosted…","i":"MMCA.Common.Testing.Architecture IntegrationEventContractTests LocalizedTextConventionTests TreatWarningsAsErrors DataResidencyTests dbo.OutboxMessages PiiConventionTests Store_Identity Store_Catalog Store_Sales MMCAStore"},{"u":"/docs/governance/store-ArchitectureScorecard.html#scorecard","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Scorecard","x":"Weighted = Maturity·weight / Implementation·weight. Axis-gap finding: §21 Accessibility is honestly M3/I8 (the chromium axe gate earns Implementation 8; Maturity caps at 3…","i":"CK_InventoryItem_AvailableQuantity_NonNegative MMCA.Store.CrossService.IntegrationTests FrameworkVersionConsistencyTests IntegrationEventContractTests.cs ConstructorDependencyCountTests StateManagementConventionTests SQLitePCLRaw.bundle_e_sqlite3 UIArchitectureConventionTests CultureInfo.InvariantCulture LocalizedTextConventionTests ObservabilityConventionTests TranslationCompletenessTests"},{"u":"/docs/governance/store-ArchitectureScorecard.html#indices","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Indices","x":"- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 313 ÷ 320 = 97.8% (re-confirmed with no moves on the 2026-08-14 re-score; §12, §21, and §22 are the three categories at…"},{"u":"/docs/governance/store-ArchitectureScorecard.html#top-5-strengths","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 strengths","x":"1. Clean Architecture + DDD + CQRS depth, fitness-enforced: §3/§4 (impl 9): inherited framework substance, guarded by the shared NetArchTest suite (23 test classes,…","i":"GracefulShutdownTests IAnonymizable"},{"u":"/docs/governance/store-ArchitectureScorecard.html#top-5-risks","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 risks","x":"1. Accessibility maturity is capped pending a human pass: §21 (mat 3, weight 3): the 23-scan axe suite gates the deploy (impl 8), but the rubric pairs axe-in-CI with a recorded…","i":"BrandColorTokenTests FormsConventionTests deploy.needs a1de5a89 MudForm"},{"u":"/docs/governance/store-ArchitectureScorecard.html#cross-repo-comparison","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Cross-repo comparison","x":"How Store relates to MMCA.Common (the framework) and MMCA.ADC (the sibling consumer) is maintained once, for all three repos, in the workspace-internal…"},{"u":"/docs/governance/store-RemediationBacklog.html","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","x":"Derived from ArchitectureScorecard.md (two-axis: Maturity 97.8% / Implementation 83.9%, full re-score 2026-07-28, re-confirmed with no score moves on the 2026-08-14 full…","i":"ArchitectureScorecard.md"},{"u":"/docs/governance/store-RemediationBacklog.html#priority-a11y--e2e-merge-gate-21-28-22","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔴 Priority: a11y / E2E merge gate (#21, #28, #22)","x":"The former single biggest maturity lever: 28 cleared 2026-07-03; 22 cleared on the 2026-07-17 re-score (the gate flip verified live) and reopened on the 2026-07-28 re-score when…","i":"github.event_name matrix.browser workflow_call deploy.needs deploy.yml browsers d057afc e2e.yml Theory needs Fact"},{"u":"/docs/governance/store-RemediationBacklog.html#priority-execution-quality-gaps-impl-not-maturity","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Priority: execution-quality gaps (impl, not maturity)","x":"Ranked 2026-07-28 when the ledger gained its second ranked axis. Until then the items in this section were closed history plus two open levers, with no ranking and no inclusion…","i":"MMCA.Store.CrossService.IntegrationTests IdentityModuleDbSeederBase.ShouldSeed SqlServerIntegrationTestFixtureBase CultureInfo.InvariantCulture MobileInfiniteScrollList ProductVariantChanged NotifyStateChanged workflow_dispatch EmailExistsAsync CatalogBrowse GetPagedAsync InventoryItem"},{"u":"/docs/governance/store-RemediationBacklog.html#priority-minor--accept-or-polish","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟡 Priority: minor / accept-or-polish","x":"- [x] 32 Dependency & Supply-Chain, impl 7 → 8. DONE (2026-07-03, drift plan D8 + D9). Vulnerability gate is NuGetAudit + TreatWarningsAsErrors at restore, which fails…","i":"ServiceInfoController TreatWarningsAsErrors BrandColorTokenTests FormsConventionTests CustomerEmailRules NuGetAuditSuppress Store_Identity Store_Catalog Store_Sales ApiVersion Deprecated MMCAStore"},{"u":"/docs/governance/store-RemediationBacklog.html#defect-fix-wave-2026-07-05","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🐞 Defect-fix wave (2026-07-05)","x":"Four reviewed product defects fixed in one wave; every behavior change flipped its pinning test in the same change. - [x] S-1 Stripe network errors escaped the Result pattern.…","i":"Payment.Stripe.SessionRetrievalFailed Payment.Stripe.SessionCreationFailed Payment.Stripe.UnsupportedCurrency CartStateService.InitializeAsync ExportUserDataHandler HttpRequestException StripePaymentService CheckoutAndPayAsync DeleteUserHandler UserRole.IsAdmin CheckoutOutcome UserRole.Admin"},{"u":"/docs/governance/store-RemediationBacklog.html#deliberate--accepted-record-the-choice-dont-silently-leave-low","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"Deliberate / accepted (record the choice; don't silently leave low)","x":"- ADR-042 device capability abstraction (latent, drift plan D8). The core extension point is converged (Store wires browser + MAUI capabilities via…","i":"AddBrowserDeviceCapabilities CultureInfo.InvariantCulture LocalizedTextConventionTests TranslationCompletenessTests UseMauiDeviceCapabilities Money.ToDisplayString ProductVariantChanged MMCA.Common.UI.Maui SliceCohesionTests DeepLinkListener ResxMudLocalizer DeviceUIModule"},{"u":"/docs/governance/store-RemediationBacklog.html#below-maturity-4-tracking-inclusion-policy-categories-scoring--4-maturity","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Below-maturity-4 tracking (inclusion policy: categories scoring < 4 maturity)","x":"These categories score maturity 3; the ledger records them per its own line-4 inclusion policy (mirrors ADC's equivalent entries). - [x] 19 · State Management & Data Flow ·…","i":"StateManagementConventionTestsBase UIArchitectureConventionTestsBase StateManagementConventionTests UIArchitectureConventionTests ProductDetail.razor.cs OrderDetail.razor.cs ProductVariantsPanel StoreArchitectureMap OrderSummaryPanel OrderLinesPanel OPERATIONS.md sloAlertSpecs"},{"u":"/docs/governance/store-RemediationBacklog.html#resolved-this-cycle-2026-07-28-drift-wave-d1d2d5d6d7--e2e4e7e8","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved this cycle (2026-07-28, drift wave: D1/D2/D5/D6/D7 + E2/E4/E7/E8)","x":"- [x] 29 Resilience: the DR drill was restoring a RETIRED database. The weekly dr-drill.yml had no rotation and fell through to its input default MMCAStore, the legacy archive no…","i":"AuthControllerBase.LoginAsync HandlerResultConventionTests PaymentReconciliationService DecoratorPipelineOrderTests PeriodicBackgroundService AddCommonRateLimiting skip_freshness_gates alertEmailAddress authIpPermitLimit Store_Identity RegisterAsync Store_Catalog"},{"u":"/docs/governance/store-RemediationBacklog.html#resolved-this-cycle-2026-07-25-performance-program-2","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved this cycle (2026-07-25, performance program 2)","x":"Second evidence-led performance pass over Common/ADC/Store. Store's share shipped as two PRs plus the v1.127.0 framework sweep. - [x] Output cache shared across replicas. Catalog…","i":"AddStackExchangeRedisOutputCache Filter.Operator.NotSupported GetVariantCartInfoHandler BulkSetInventoryHandler IProductVariantService GetUnitPricesAsync IDistributedCache IntFilterStrategy OrderLines.Count PaymentInitiated EvictByTagAsync ProductVariants"},{"u":"/docs/governance/store-RemediationBacklog.html#resolved-this-cycle-2026-07-11-drift-convergence-drift-plan-d1-d13","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved this cycle (2026-07-11 drift-convergence, drift plan D1-D13)","x":"- [x] 29 DR gates (drift plan D3). dr-freshness is now in deploy.needs (fails a deploy when the last successful dr-drill is stale), dr-drill.yml gained a weekly cron, and…","i":"ConstructorDependencyCountTests MMCA.Store.Gateway.Tests GracefulShutdownTests MMCA.Store.CI.slnf Store_Identity Store_Catalog workflow_call deploy.needs TimeProvider Store_Sales Directory db_owner"},{"u":"/docs/governance/store-RemediationBacklog.html#already-at-level-4-protect-dont-regress","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"✅ Already at level 4 (protect, don't regress)","x":"Both axes satisfied (maturity 4 AND implementation = 9), the true protect list: SOLID (1), Design Patterns (2), Clean Architecture (3), DDD (4), Data (8), API (9), Observability…","i":"CK_InventoryItem_AvailableQuantity_NonNegative FormsConventionTests IQueryable"},{"u":"/docs/guides/index.html","d":"Guides & Specifications","k":"Guides & Specifications","x":"The narrative documentation for the MMCA platform: adoption guides, business specifications, workflow analyses, and per-concern reference notes. Files are prefixed by the repo…"},{"u":"/docs/guides/index.html#framework-mmcacommon","d":"Guides & Specifications","k":"Guides & Specifications","t":"Framework (MMCA.Common)","x":"- Getting Started: stand up a new application from the MMCA.Templates scaffold, in six steps. - Build MMCA.ECommerce: the two-module store sample (Products + Orders) built end to…","i":"MMCA.Templates"},{"u":"/docs/guides/index.html#mmcastore-e-commerce","d":"Guides & Specifications","k":"Guides & Specifications","t":"MMCA.Store (e-commerce)","x":"- Business Specification - Business Workflow Analysis - Navigation Flow - Manual Screen-Reader Pass Runbook"},{"u":"/docs/guides/index.html#mmcaadc-conference","d":"Guides & Specifications","k":"Guides & Specifications","t":"MMCA.ADC (conference)","x":"- Business Specifications - Navigation Flow - Manual Screen-Reader Pass Runbook - Integration-Test Tier Rework Plan Related reading: the Architecture Decision Records and the…"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","x":"The automated layer (axe-core WCAG 2.1 AA scans in Tests/E2E/MMCA.ADC.E2E.Tests/AccessibilityTests.cs plus the shared Login/Register/Profile bases in MMCA.Common.Testing.E2E)…","i":"MMCA.Common.Testing.E2E RemediationBacklog.md"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#tools","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Tools","x":"Run against the app started via Aspire (dotnet run --project Source/Hosting/MMCA.ADC.AppHost), reaching the UI through the Gateway. Test with the keyboard only (no mouse) for the…","i":"dotnet start Ctrl Alt Cmd run"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#what-to-verify-on-every-flow","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"What to verify on every flow","x":"1. Landmarks and headings. The SR's landmark/heading list (NVDA Insert+F7) exposes a logical banner / navigation / main structure and a sensible h1...hN outline (one h1 per…","i":"MainLayout.razor navigation h1...hN banner Insert Shift main Tab"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#results-log","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Results log","x":"Record one row per pass. A flow with any unresolved blocker is a FAIL (open a backlog item for it)."},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#after-a-pass","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"After a pass","x":"- File any defect as a remediation item; cross-link it from the §21 row's evidence. - Update the dated row above so the scorecard can cite a real, current manual pass (this is…"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","x":"Status: complete (Phase 4 broker-transport tier landed 2026-07-06; Phase 5 residual = coverlet only). - Phase 0 ✅: Tests/WebAPI revived as MMCA.Common.API middleware unit tests…","i":"Microsoft.Testing.Extensions.CodeCoverage ISessionBookmarkValidationService IdentityIntegrationTestFixture SpeakerUnlinkedFromUserHandler AnonymousConferenceReadTests SpeakerLinkedToUserHandler MMCA.ADC.Integration.slnf MMCA.ADC.IntegrationTests IIntegrationEventHandler AddForwardedJwtBearer AttendeeBookmarkTests IBookmarkCountService"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#recommended-strategy-two-tiers","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Recommended strategy: two tiers","x":"1. Primary: per-service WebApplicationFactory : one in-process host per service (Identity / Conference / Engagement), cross-service edges mocked. AddBrokerMessaging…","i":"DistributedApplicationTestingBuilder SpeakerUnlinkedFromUser WebApplicationFactory SpeakerLinkedToUser AddBrokerMessaging UserRegistered Program"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#three-code-facts-that-shape-the-rework-verified","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Three code facts that shape the rework (verified)","x":"- Only Conference.Service is WAF-incompatible: it ends with StartAsync() + self-HTTP/2 WarmupViaHttpAsync + WaitForShutdownAsync(). Identity/Engagement/Notification use…","i":"AddCommonAuthentication AddForwardedJwtBearer WebApplicationFactory WaitForShutdownAsync Conference.Service WarmupViaHttpAsync JwtTokenGenerator IssuerSigningKey JwtBearerOptions app.RunAsync StartAsync authority"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#databases","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Databases","x":"- SQLite in-memory for the fast bulk tier (no Docker, CI-friendly; DatabaseInitStrategy=EnsureCreated). - MsSql Testcontainers for a tagged SQL-fidelity subset (soft-delete…","i":"SQLServerDbContext DataSources migrations"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#project-structure","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Project structure","x":"- One WAF test project per service (MMCA.ADC.{Identity,Conference,Engagement}.IntegrationTests): can't reference two Program-bearing hosts in one project. - One…","i":"MMCA.ADC.CrossService.IntegrationTests IntegrationTestBase MMCA.Common.Testing JwtTokenGenerator IntegrationTests ProjectReference MMCA.Common.API WebAPI.Tests Conference Engagement Identity MMCA.ADC"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#ci","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"CI","x":"- Add the SQLite per-service tier to CI.slnf (seconds, no Docker) → restores the authz/CRUD merge gate (11) with no workflow change. - Keep the container-based MsSql + RabbitMQ…","i":"CI.slnf"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#phased-sequencing-fastest-win-first","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Phased sequencing (fastest win first)","x":"- Phase 0: re-home WebAPI.Tests middleware unit tests; drop the dead WebAPI reference; re-add to slnx+CI.slnf. ~16 tests green; removes a non-building project (16). - Phase 1:…","i":"ISessionBookmarkValidationService IBookmarkCountService OwnerOrAdminFilter ServiceTestFixture JwtBearerOptions AttendeeClaims OrganizerUser WebAPI.Tests TProgram CI.slnf slnx"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#key-risks","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Key risks","x":"- The non-Identity JwtBearerOptions in-process override is the trickiest piece: prove it on one Conference auth test before fanning out. - SQLite vs SQL-Server fidelity (owned…","i":"JwtBearerOptions"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#critical-files","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Critical files","x":"- Tests/Integration/MMCA.ADC.IntegrationTests/Infrastructure/TestWebApplicationFactory.cs (combined-host factory → split into per-service fixtures; its JWT config block is the…","i":"AddCommonAuthentication AddForwardedJwtBearer JwtTokenGenerator.cs MMCA.ADC.CI.slnf MMCA.ADC.slnx StartAsync partial Program public class"},{"u":"/docs/guides/adc-NavigationFlow.html","d":"Navigation Flow","k":"Guides & Specifications","x":"This document maps the site navigation flow for each actor in the MMCA.ADC application. Each mermaid diagram shows the pages accessible to that actor and the directional…"},{"u":"/docs/guides/adc-NavigationFlow.html#actors","d":"Navigation Flow","k":"Guides & Specifications","t":"Actors","x":"Roles & menu: Organizer is the only elevated role (default is Attendee). A Speaker is an attendee whose account is linked to a Speaker, surfaced via the speakerid claim. The left…","i":"IUIModule.NavItems speaker_id Organizer Attendee"},{"u":"/docs/guides/adc-NavigationFlow.html#1-anonymous-user","d":"Navigation Flow","k":"Guides & Specifications","t":"1. Anonymous User","x":"Pages accessible without authentication: home, login, register, the two password-reset pages, and all public conference pages. ---"},{"u":"/docs/guides/adc-NavigationFlow.html#2-attendee-authenticated-user","d":"Navigation Flow","k":"Guides & Specifications","t":"2. Attendee (Authenticated User)","x":"Inherits all anonymous pages. Gains access to profile, feedback submission, and session bookmarking. Unauthenticated visitors are redirected to login. ---"},{"u":"/docs/guides/adc-NavigationFlow.html#3-speaker","d":"Navigation Flow","k":"Guides & Specifications","t":"3. Speaker","x":"Inherits all attendee pages. Gains access to the speaker dashboard for managing their own profile, viewing assigned sessions, and reviewing feedback ratings. ---"},{"u":"/docs/guides/adc-NavigationFlow.html#4-organizer","d":"Navigation Flow","k":"Guides & Specifications","t":"4. Organizer","x":"Authenticated users with the Organizer role. Inherits all attendee and public pages. Adds CRUD management for every conference entity (events, sessions, speakers, categories,…","i":"Organizer"},{"u":"/docs/guides/adc-NavigationFlow.html#5-functionality-flows-attendee--speaker","d":"Navigation Flow","k":"Guides & Specifications","t":"5. Functionality Flows (Attendee & Speaker)","x":"The diagrams in sections 1-4 map which pages each actor can reach. The diagrams below map how attendees and speakers accomplish each functionality, including inline actions…","i":"DeviceUIModule speaker_id route"},{"u":"/docs/guides/adc-NavigationFlow.html#navigation-patterns","d":"Navigation Flow","k":"Guides & Specifications","t":"Navigation Patterns","x":"- Unauthenticated users accessing protected pages are redirected to /login via the RedirectToLogin component. - Successful login/register redirects to Home (/) with a full page…","i":"RegisteredUser_AdminPages_ShouldBeForbidden Engagement.CheckIn IUIModule.NavItems Engagement.Points EventList.razor RedirectToLogin DeviceUIModule UserList.razor Routes.razor speaker_id attribute Authorize"},{"u":"/docs/guides/adc-specifications.html","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","x":"---"},{"u":"/docs/guides/adc-specifications.html#1-system-overview","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"1. System Overview","x":"ADC is a conference management system for the Atlanta Developers Conference. It provides backend services to manage multi-day conference events, sessions, speakers, rooms,…"},{"u":"/docs/guides/adc-specifications.html#2-domain-model","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"2. Domain Model","x":"Relationships: - Owns many Rooms (child entities) - Owns many EventSpeakers (child join entities linking Event ↔ Speaker) - Owns many EventQuestionAnswers (child feedback…","i":"Engagement.LivePolls Engagement.SessionQA User.LinkedSpeakerId Event.StartDate Session.EventId ContentEditor Event.EndDate EventSpeaker QuestionType Waitlisted Nominated Organizer"},{"u":"/docs/guides/adc-specifications.html#3-business-rules","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"3. Business Rules","x":"Reading guide: Some rules reference other rules defined later in the document (e.g., BR-63, BR-80 are defined in Section 10). Forward references use the BR- numbering…","i":"Event.QuestionModerationDefault Event.LastSessionizeRefreshBy Event.LastSessionizeRefreshOn SessionQuestionAnswerChanged SpeakerQuestionAnswerChanged EventQuestionAnswerChanged SessionCategoryItemChanged SpeakerCategoryItemChanged UserSessionBookmarkChanged Session.AccessibilityInfo Session.IsServiceSession SessionFeedbackSubmitted"},{"u":"/docs/guides/adc-specifications.html#4-use-cases--business-processes","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"4. Use Cases / Business Processes","x":"See UC-30 (Registration) and UC-31 (Login) in Section 12.2 for the current email + password authentication flows. UC-34 (Request password reset) and UC-35 (Reset password) in the…","i":"SpeakerQuestionAnswersController Event.LastSessionizeRefreshBy Event.LastSessionizeRefreshOn SpeakerQuestionAnswerChanged Engagement.LivePolls Engagement.SessionQA UserSessionBookmark skippedSoftDeleted IsServiceSession IsPlenumSession AllowAnonymous QuestionEntity"},{"u":"/docs/guides/adc-specifications.html#5-workflows--state-transitions","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"5. Workflows & State Transitions","x":"The Session.Status field is a free-text string imported from Sessionize. Default: null (for manually created sessions). Known Sessionize values: Accepted, Waitlisted, Accept…","i":"Session.Status ContentEditor IsConfirmed IsInformed Waitlisted Nominated Organizer Accepted Declined Decline Accept Queue"},{"u":"/docs/guides/adc-specifications.html#6-events--side-effects","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"6. Events & Side Effects","x":"Domain events are raised for entity mutations. Not all events have registered handlers: events without handlers serve as extension points for future requirements. Note: Only…","i":"SessionQuestionAnswerChanged SpeakerQuestionAnswerChanged EventQuestionAnswerChanged SessionCategoryItemChanged SpeakerCategoryItemChanged UserSessionBookmarkChanged SessionSpeakerChanged User.LinkedSpeakerId CategoryItemChanged EventSpeakerChanged UserPasswordChanged CategoryChanged"},{"u":"/docs/guides/adc-specifications.html#7-business-constraints--invariants","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"7. Business Constraints & Invariants","x":"---","i":"Speaker.LinkedUserId User.LinkedSpeakerId IsServiceSession Session.EventId QuestionEntity ContentEditor EventSpeaker nameProperty Waitlisted CreatedBy FirstName Nominated"},{"u":"/docs/guides/adc-specifications.html#8-external-integrations-business-perspective","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"8. External Integrations (Business Perspective)","x":"---","i":"Speaker.ProfilePicture Event.VenueMapUrl IsServiceSession IsPlenumSession QuestionSource SessionizeCode IsTopSpeaker RecordingUrl LiveUrl POST"},{"u":"/docs/guides/adc-specifications.html#9-glossary","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"9. Glossary","x":"---","i":"Event.IsPublished IsServiceSession IsPlenumSession ContentEditor IsTopSpeaker Organizer User.Role Admin Role true"},{"u":"/docs/guides/adc-specifications.html#ddd-structural-summary","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"DDD Structural Summary","x":"Why three bounded contexts instead of two: The original Events + Identity split grouped all conference-related entities together regardless of write profile. Separating…","i":"SessionQuestionAnswer SpeakerQuestionAnswer EventQuestionAnswer Question.IsRequired Session.EventId QuestionEntity SpeakerChanged ContentEditor QuestionType Room.EventId RoomChanged Organizer"},{"u":"/docs/guides/adc-specifications.html#10-specification-clarifications--addenda","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"10. Specification Clarifications & Addenda","x":"This section addresses gaps, ambiguities, and implicit design decisions identified during implementation review. New business rules are numbered BR-61+. API contract…","i":"SessionQuestionAnswersController SpeakerQuestionAnswersController TimeZoneInfo.ConvertTimeFromUtc EventQuestionAnswersController MMCA.ADC.Modules.Engagement RemoveSpeakerQuestionAnswer UpdateSpeakerQuestionAnswer AddSpeakerQuestionAnswer SessionFeedbackSubmitted EventFeedbackSubmitted CreateQuestionHandler SessionQuestionAnswer"},{"u":"/docs/guides/adc-specifications.html#11-api-contract-specifications","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"11. API Contract Specifications","x":"This section documents API design decisions that apply across all endpoints. --- All error responses use the RFC 9457 ProblemDetails format (the successor to RFC 7807, same…","i":"SessionQuestionAnswer SpeakerQuestionAnswer EventQuestionAnswer PaginationMetadata Session.Duration Speaker.FullName DomainException includeChildren FirstRowOnPage LastModifiedOn QuestionEntity TotalPageCount"},{"u":"/docs/guides/adc-specifications.html#12-authentication--identity-architecture","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"12. Authentication & Identity Architecture","x":"This section defines the authentication mechanism for both the Web UI (Blazor) and MAUI (mobile) clients, which share a common Razor class library. It replaces the device-based…","i":"CascadingAuthenticationState AuthenticationStateProvider PasswordReset__ResetUrl PasswordResetController Auth.InvalidResetToken MaxValidationAttempts RequestWindowMinutes Speaker.LinkedUserId TokenLifetimeMinutes User.LinkedSpeakerId MaxRequestsPerEmail UserPasswordChanged"},{"u":"/docs/guides/common-ACCESSIBILITY.html","d":"Accessibility (rubric §21)","k":"Guides & Specifications","x":"The shared MMCA.Common.UI surface targets WCAG 2.1 AA. Accessibility is enforced two ways: an automated axe-core gate in CI (the bulk of coverage) and a documented manual…","i":"MMCA.Common.UI"},{"u":"/docs/guides/common-ACCESSIBILITY.html#automated-coverage-axe-core-wcag-21-aa","d":"Accessibility (rubric §21)","k":"Guides & Specifications","t":"Automated coverage (axe-core, WCAG 2.1 AA)","x":"The ui-e2e CI job runs Playwright + axe-core against the backend-less gallery; chromium is the blocking merge gate (firefox/webkit advisory). Scanned states: Component render is…","i":"PrimitivesSnapshotTests RegisterPageE2ETests PrimaryContrastText ErrorContrastText DarkModeE2ETests PageLoadingState MMCA.Common.UI progressbar mmca_theme div"},{"u":"/docs/guides/common-ACCESSIBILITY.html#manual-screen-reader-pass","d":"Accessibility (rubric §21)","k":"Guides & Specifications","t":"Manual screen-reader pass","x":"Automation cannot judge reading order, focus management, or announcement quality, so the shared surface is walked manually. Checklist (re-run on any change to MainLayout, the…","i":"ValidationMessage MainLayout.razor PageLoadingState MainLayout EditForm main"},{"u":"/docs/guides/common-ACCESSIBILITY.html#known-limitations-tracked","d":"Accessibility (rubric §21)","k":"Guides & Specifications","t":"Known limitations (tracked)","x":"- ~~Dark-mode contrast (§20, not §21).~~ RESOLVED (2026-07-11). The two dark-palette WCAG AA contrast failures the prototype scan flagged (filled-primary button label ~2.65:1 on…","i":"PaletteDark.PrimaryContrastText WarningContrastText ErrorContrastText DarkModeE2ETests EF5350 rgba"},{"u":"/docs/guides/common-BUILD-BY-HAND.html","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","x":"This is the long-form walkthrough: every project, every file, and every load-bearing line that goes into an application on the MMCA.Common framework, in the order you would…","i":"Contoso.Support Tickets dotnet Orders Order new"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#what-you-will-build","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"What you will build","x":"A modular monolith with one business module and two hosts: - Orders (your business module): an Order aggregate with OrderComment children, opened through a Result-returning…","i":"AllowAnonymous OrderComment Result Order sql web"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-0-prerequisites-and-decisions","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 0: Prerequisites and decisions","x":"Install: - .NET 10 SDK (the framework targets net10.0 with LangVersion: preview for C extension types). - SQL Server reachable locally (LocalDB, a container, or the one Aspire…","i":"FrameworkVersionConsistencyTestsBase Directory.Packages.props MMCA.Common.API UseLocalMMCA LangVersion local.props install net10.0 package preview CS0103 dotnet"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-1-create-the-solution-and-the-build-plumbing","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 1: Create the solution and the build plumbing","x":"Scaffolded. dotnet new mmca-app writes every file in this phase. Read it to know what each one does; you do not need to type any of it. The plumbing files are the load-bearing,…","i":"Directory.Packages.props Directory.Build.props Contoso.Support.slnx local.props.template OrderIdentifierType PackageReference MMCA.Helpdesk auditSources editorconfig nuget.config global.json Contracts"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-2-scaffold-the-module-project-set","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 2: Scaffold the module project set","x":"Scaffolded. dotnet new mmca-app creates this project set for your first module, and pwsh build/add-module.ps1 adds another one later: it drives dotnet new mmca-module and then…","i":"Contoso.Support.Orders.Infrastructure Contoso.Support.Orders.Application Contoso.Support.Orders.Domain Contoso.Support.Orders.Shared Contoso.Support.Orders.API MMCA.Common.Infrastructure MMCA.Common.Application MMCA.Common.Domain MMCA.Common.Shared AddErrorResources MMCA.Common.API AllowAnonymous"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-3-the-vertical-slice-end-to-end-the-heart-of-it","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 3: The vertical slice end-to-end (the heart of it)","x":"Scaffolded. The generated module already contains this slice and six more, worked end to end. dotnet new mmca-command and dotnet new mmca-query add another one. This phase is the…","i":"EntityTypeConfigurationSQLServer ConcurrencyConventionTestsBase AddModuleOrdersInfrastructure ScanModuleApplicationServices AuditableAggregateRootEntity OrderOpenedIntegrationEvent OrderCommentIdentifierType IUnitOfWork.GetRepository AddApplicationDecorators IIntegrationEventHandler DomainEventDispatcher EntityControllerBase"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-4-dbcontext-model-and-migrations","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 4: DbContext model and migrations","x":"Partly scaffolded. The migrations project and its design-time factory are generated. Running dotnet ef migrations add InitialCreate is still yours, and for a module added later…","i":"ApplicationSettings.DatabaseInitStrategy InitializeDatabaseAsync SQLServerDbContext EnsureCreated InitialCreate DataSources migrations Migrate dotnet None add"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-5-compose-the-monolith-host-and-run-it","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 5: Compose the monolith host and run it","x":"Scaffolded. Both hosts, the AppHost, and the .resx pairs are generated. Read this phase before you touch any of them: the DI sequence, WaitFor(sql) rather than the database…","i":"MmcaCultureBootstrap.SetBrowserCultureAsync LocalizedTextConventionTestsBase LocalizationResourceTestsBase UseCommonRequestLocalization OrderOpenedIntegrationEvent UseCommonMiddlewarePipeline services.AddErrorResources AddApplicationDecorators YourModuleErrorResources EnsureSuccessStatusCode EndpointCultureApplier UseRequestLocalization"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-6-tests-and-the-architecture-fitness-map","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 6: Tests and the architecture-fitness map","x":"Scaffolded, with one deliberate gap. All three test projects and the map are generated. The IntegrationEventContractTests subclass is NOT: its frozen literal lists members…","i":"FrameworkVersionConsistencyTestsBase ConstructorDependencyCountTestsBase IntegrationEventContractTestsBase LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture SpecificationConventionTestsBase MicroserviceExtractionTestsBase ConcurrencyConventionTestsBase ControllerConventionTestsBase IntegrationEventContractTests LocalizationResourceTestsBase HandlerConventionTestsBase"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-7-upgrading-the-framework-version","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 7: Upgrading the framework version","x":"Not scaffolded. dotnet new mmca-app --framework-version picks the version you START on; moving to a later one is this phase. When a new MMCA.Common release ships, upgrade in one…","i":"FrameworkVersionConsistencyTestsBase Directory.Packages.props packages.lock.json UseLocalMMCA local.props your.slnx restore dotnet new"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-8-extract-a-module-into-its-own-service-the-payoff","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 8: Extract a module into its own service (the payoff)","x":"Not scaffolded. The generated solution carries the plumbing (the .Contracts proto convention and the .Service OpenAPI block in Directory.Build.props), but the extraction itself…","i":"GrpcResultExceptionInterceptor OrderOpenedIntegrationEvent MMCA.Common.Aspire.Hosting WithSQLServerDataSource AddGrpcServiceDefaults Directory.Build.props RequestVersionExact AddTypedGrpcClient WithJwksDiscovery MMCA.Common.Grpc Support_Identity OutboxMessages"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#verification-checklist","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Verification checklist","x":"1. Build green: dotnet build Contoso.Support.slnx with no warnings (TreatWarningsAsErrors + five analyzers). This is the primary automatable gate. 2. Unit + architecture tests…","i":"OrderOpenedIntegrationEvent Contoso.Support.slnx IArchitectureMap OutboxMessages InitialCreate OrderComment migrations AppHost dotnet build Order test"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#where-to-look-next","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Where to look next","x":"- Getting Started: the one-command path that writes phases 1 through 6 for you. If you are starting a new solution rather than adding the framework to an existing one, that is…","i":"CLAUDE.md README.md Helpdesk Tickets Ticket"},{"u":"/docs/guides/common-COST.html","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","x":"MMCA.Common is a library, so it cannot provision anything: right-sizing, scale rules, budgets, and per-service cost attribution live in the consumer apps' IaC (e.g. MMCA.ADC's…"},{"u":"/docs/guides/common-COST.html#what-the-framework-does-for-cost","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"What the framework does for cost","x":"- Telemetry ingestion is the real line item, so high-volume / low-value spans are dropped. OutboxPollFilterProcessor (MMCA.Common.Aspire) suppresses the recurring OutboxPoll…","i":"http.client.open_connections OutboxPollFilterProcessor TraceIdRatioBasedSampler ConfigureOpenTelemetry OutboxCleanupService AddServiceDefaults MMCA.Common.Aspire ParentBasedSampler SocketsHttpHandler request.duration active_requests AppDependencies"},{"u":"/docs/guides/common-COST.html#recommended-consumer-defaults-set-these-downstream","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"Recommended consumer defaults (set these downstream)","x":"- Telemetry retention & sampling. Tune Log Analytics retention to the minimum the consumer's compliance window allows, and set Telemetry:TracesSampleRatio (the built-in…"},{"u":"/docs/guides/common-COST.html#cost-attribution--guardrail-samples-distilled-from-mmcaadc","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"Cost-attribution & guardrail samples (distilled from MMCA.ADC)","x":"These belong in the consumer's IaC, not the library, but the framework documents the shape so every consumer attributes spend and guards surges the same way. The worked, deployed…"},{"u":"/docs/guides/common-COST.html#out-of-scope-for-the-framework-by-design","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"Out of scope for the framework (by design)","x":"Provisioning, scale rules, budgets, per-service cost attribution, and surge/revert automation are consumer/IaC concerns and are not added to the library: see also ADR-009…"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","x":"MMCA.ECommerce is the simplest e-commerce application on the MMCA.Common framework: a Products catalog module and an Orders module with line items, behind a REST API host and a…","i":"MMCA.Templates dotnet new"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#before-you-start","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"Before you start","x":"- .NET 10 SDK (the framework targets net10.0 with LangVersion: preview). - Docker Desktop (Aspire provisions SQL Server as a container). - EF Core tools: dotnet tool install…","i":"MMCA.Templates LangVersion install net10.0 preview dotnet pwsh tool ps1"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#2-generate-the-solution-with-the-products-module","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"2. Generate the solution with the Products module","x":"Five options do most of this guide's old work. Three remove an axis a catalog product does not have, and the code for an axis you turn off is never generated: --flat drops the…","i":"ProductCreatedIntegrationEvent ProductCreatedHandler RequesterUserId Created Opened Name"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#3-add-the-orders-module","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"3. Add the Orders module","x":"build/add-module.ps1 ships inside the solution you just generated. It runs dotnet new mmca-module with the shape options passed through, then performs every wire-up the template…","i":"ECommerceArchitectureMap.cs SQLServerMigrationsAssembly services.AddErrorResources OrderItemIdentifierType WithSQLServerDataSource Directory.Build.props OrdersErrorResources MMCA.ECommerce.slnx ChangeItemQuantity ECommerce_Products appsettings.json ProjectReference"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#4-reshape-products-into-a-catalog-product","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"4. Reshape Products into a catalog product","x":"The scaffolded module arrives as the template's worked example in your namespaces, already shaped by the flags in step 2: no children, no status, no requester, Name instead of…","i":"UpdateRequestsAreConcurrencyAware Microsoft.EntityFrameworkCore Product.Description.TooLong ModuleApplicationDbContext ProductCreateRequestMapper DomainEntityState.Updated Product.Description.Empty Directory.Packages.props DependencyInjection.cs TreatWarningsAsErrors Product.InvalidPrice Product.Name.TooLong"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#5-reshape-orders-into-an-order-with-line-items","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"5. Reshape Orders into an order with line items","x":"Orders keeps the child-collection pattern the template scaffolded, retargeted. -Child Item already did the naming (the entity is OrderItem, the slices are AddItem / EditItem /…","i":"UpdateRequestsAreConcurrencyAware Order.Item.ProductName.TooLong Total_ExcludesSoftDeletedItems EnsureStatusAllowsItemChanges Microsoft.EntityFrameworkCore Order.InvalidStatusTransition Order.Item.ProductName.Empty ChangeOrderStatusRequest.cs OrderPlacedIntegrationEvent ModuleApplicationDbContext Order.CustomerName.TooLong ChangeItemQuantityCommand"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#6-point-the-ui-at-the-new-domain","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"6. Point the UI at the new domain","x":"The scaffolded Blazor host already has the load-bearing parts: the typed ECommerceApiClient calling the API server-side through Aspire service discovery (no CORS, no token), the…","i":"MMCA.ECommerce.Orders.Shared string.IsNullOrWhiteSpace Snackbar.RequiredFields Dialog.Delete.Heading System.Globalization GetProductsAsync ProjectReference missingRequired SectionHeading PageHeading es.resx _field"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#7-create-the-migrations","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"7. Create the migrations","x":"Neither module has a migration yet: any shape flag makes mmca-app drop the template's sample one (it described the sample shape), and -SkipMigration deferred the Orders one to…","i":"editorconfig migrations Migrations dotnet add"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#8-the-two-one-time-fixups-then-run-it","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"8. The two one-time fixups, then run it","x":"Apply the two fixups the scaffold deliberately leaves to you (they are name-dependent, so no generated value could be right). First, sort the using directives and the identifier…","i":"ProductCreatedIntegrationEvent IntegrationEventContractTests OrderPlacedIntegrationEvent ArchitectureTests.cs AllowAnonymous editorconfig SCAFFOLD IDE0021 SA1210 SA1211 DELTA Open"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#verification-checklist","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"Verification checklist","x":"1. Baseline green immediately after mmca-app, before any edit: 81 tests. 2. After build/add-module.ps1: still green at 99 tests, both modules' scaffolded suites running. 3. After…","i":"MMCA.ECommerce.slnx OutboxMessages InitialCreate dotnet build test"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#where-to-look-next","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"Where to look next","x":"- MMCA.ECommerce: the finished result of this guide, build- and test-verified. - Getting started: the single-module path, the vertical-slice templates (mmca-command /…"},{"u":"/docs/guides/common-GETTING-STARTED.html","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","x":"MMCA.Common is a .NET 10 framework for DDD, Clean Architecture, and CQRS, shipped as a set of lockstep-versioned NuGet packages (the authoritative list and count live in…"},{"u":"/docs/guides/common-GETTING-STARTED.html#before-you-start","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Before you start","x":"- .NET 10 SDK. The framework targets net10.0 with LangVersion: preview for C extension types. - Docker Desktop. Aspire provisions SQL Server as a container, so you do not install…","i":"MMCA.Templates LangVersion install net10.0 preview dotnet tool"},{"u":"/docs/guides/common-GETTING-STARTED.html#1-install-the-template-pack","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"1. Install the template pack","x":"Four templates arrive: mmca-app (a whole solution), mmca-module (a business module across all five layers), and mmca-command / mmca-query (a single vertical slice)."},{"u":"/docs/guides/common-GETTING-STARTED.html#2-generate-the-solution","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"2. Generate the solution","x":"Three names, and they are independent: the solution (also your root namespace), the first module in plural PascalCase, and that module's aggregate root in singular PascalCase.…","i":"ProjectReference local.props Billing Invoice"},{"u":"/docs/guides/common-GETTING-STARTED.html#3-build-and-test-before-you-change-anything","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"3. Build and test before you change anything","x":"That is a warning-free build with TreatWarningsAsErrors and all five analyzers at error severity, and a passing test run including the architecture-fitness rules, with no…","i":"TreatWarningsAsErrors"},{"u":"/docs/guides/common-GETTING-STARTED.html#4-create-the-first-migration","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"4. Create the first migration","x":"The scaffold ships the migrations project and its design-time factory; the migration itself describes your entities, so it is yours to generate: Always pass --context…","i":"SQLServerDbContext DbSet"},{"u":"/docs/guides/common-GETTING-STARTED.html#5-run-it","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"5. Run it","x":"Run this from a real, interactive terminal. Launched from a headless or background shell the Aspire AppHost stalls at control-plane init and no dashboard appears. The dashboard…","i":"OrderOpenedIntegrationEvent AllowAnonymous POST GET sql web"},{"u":"/docs/guides/common-GETTING-STARTED.html#6-the-two-one-time-fixups","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"6. The two one-time fixups","x":"The scaffold deliberately does not hand these over, because renaming invalidates them and no fixed value is right for every name you could pick. Both are covered in full in the…","i":"IntegrationEventContractTestsBase Contoso.Support.Orders.Shared Zeta.App.Orders.Shared ArchitectureTests.cs editorconfig SCAFFOLD IDE0021 SA1211 Ticket DELTA using"},{"u":"/docs/guides/common-GETTING-STARTED.html#what-you-were-handed","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"What you were handed","x":"The Order aggregate arrives fully worked: a Result-returning factory, invariants, guarded mutations raising domain events, a child entity, soft-delete cascade, the caching pair,…","i":"AddApplicationDecorators Directory.Build.props OrderIdentifierType IArchitectureMap HandleFailure ModuleLoader ErrorType WaitFor global Result DbSet Order"},{"u":"/docs/guides/common-GETTING-STARTED.html#add-your-next-feature","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Add your next feature","x":"A vertical slice (the path every feature follows) is one command, run from the module's UseCases folder: Handlers, validators, and mappers are convention-scanned, so there is no…","i":"order.TransferToRequester AddErrorResources RequesterUserId AddDomainEvent Result.Combine ChangeStatus GetByIdAsync SaveChanges definition IsFailure CacheKey Comments"},{"u":"/docs/guides/common-GETTING-STARTED.html#surface-the-slice-at-the-edge","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Surface the slice at the edge","x":"The scaffold stops at the handler, and the template's closing instructions tell you to map the command in your module's controller. Every write in the generated app follows the…","i":"ThrowIfDomainExceptionAsync _transferRequesterUserId ChangeOrderStatusRequest Api.TransferOrderAsync EntityControllerBase TransferOrderCommand OrderDetail.es.resx ICacheInvalidating ChangeStatusAsync OrderDetail.razor OrderDetail.resx SupportApiClient"},{"u":"/docs/guides/common-GETTING-STARTED.html#then-what","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Then what","x":"- Upgrade the framework. Bump every MMCA.Common. entry in Directory.Packages.props together, in one pass. See Phase 7 and the versioning policy. - Add real authentication. Copy…","i":"Directory.Packages.props Authorize Contracts Service"},{"u":"/docs/guides/common-GETTING-STARTED.html#verification-checklist","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Verification checklist","x":"1. dotnet new mmca-app -n produced a solution that builds and tests green before you changed anything. 2. dotnet build .slnx is warning-free (TreatWarningsAsErrors + five…","i":"OutboxMessages InitialCreate migrations healthy YourApp dotnet build slnx test then add new"},{"u":"/docs/guides/common-GETTING-STARTED.html#where-to-look-next","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Where to look next","x":"- Templates: every parameter of all four templates, dropping the Blazor UI host, and how the pack is built. ADR-065 explains why it is derived from the reference app rather than…"},{"u":"/docs/guides/common-RESILIENCE.html","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","x":"MMCA.Common is a library, so it cannot operate a deployment: restores, RTO/RPO, and SLO alerting are executed in the consumer apps' IaC (e.g. MMCA.ADC's…"},{"u":"/docs/guides/common-RESILIENCE.html#what-the-framework-provides-and-verifies-in-repo","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","t":"What the framework provides (and verifies in-repo)","x":"Failure isolation, graceful degradation, graceful startup, and the restore procedure itself are therefore demonstrated and tested centrally: the framework drills backup→restore…","i":"ResilienceCircuitBreakerFaultInjectionTests OpenIdConnectMetadataWarmupTask WarmupReadinessHealthCheckTests AddStandardResilienceHandler ConfigureHttpClientDefaults WarmupReadinessHealthCheck DatabaseRestoreDrillTests ConfigureBrokerTransport WarmupHostedServiceTests WarmupReadinessGateTests ResilienceHandlerTests WarmupHostedService"},{"u":"/docs/guides/common-RESILIENCE.html#baseline-slo--error-budget-template-consumers-fill-in","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","t":"Baseline SLO / error-budget template (consumers fill in)","x":"Adopt and tune per app; ADC's filled-in version lives in infra/DISASTER-RECOVERY.md + the SLO metric-alerts in infra/main.bicep. Define RTO/RPO per service (ADC's worked…","i":"requests"},{"u":"/docs/guides/common-RESILIENCE.html#restore-drill-runbook-reference","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","t":"Restore-drill runbook (reference)","x":"The only evidence backups actually restore is a periodic drill: restore a throwaway copy, confirm it comes back Online, record the measured restore time, then delete the copy.…"},{"u":"/docs/guides/common-RESPONSIVE.html","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","x":"This document is the supported-device and browser matrix for the shared MMCA.Common.UI component library. It makes the responsive contract explicit (the rubric §22 note that it…","i":"MMCA.Common.UI"},{"u":"/docs/guides/common-RESPONSIVE.html#breakpoints","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Breakpoints","x":"The framework keeps C viewport detection and CSS media queries aligned around one mobile threshold. The C 960px mobile cutoff and the CSS 1023.98px cutoff intentionally differ:…","i":"BreakpointConstants.IsMobileBreakpoint media i.e"},{"u":"/docs/guides/common-RESPONSIVE.html#touch-targets","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Touch targets","x":"Interactive controls on mobile surfaces meet a 48px minimum hit area (Material Design), exceeding both WCAG 2.5.8 Target Size (Minimum, AA, 24px) and WCAG 2.5.5 Target Size…"},{"u":"/docs/guides/common-RESPONSIVE.html#grid-density","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Grid density","x":"DataGridListPageBase exposes a DenseGrid property and a ToggleDensity() method. Derived list pages bind Dense=\"@DenseGrid\" on their MudDataGrid and surface a toggle. The chosen…","i":"ListPageQueryStateServiceTests ListPageStateServiceTests DataGridListPageBase ToggleDensity MudDataGrid DenseGrid TDto"},{"u":"/docs/guides/common-RESPONSIVE.html#browser-matrix","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Browser matrix","x":"The shared UI is tested against three Playwright engines in CI (.github/workflows/ci.yml, ui-e2e job): a real-browser axe (WCAG 2.1 AA) + render smoke against the backend-less…","i":"false"},{"u":"/docs/guides/common-TEMPLATES.html","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","x":"MMCA.Templates is a dotnet new pack that scaffolds solutions, modules, and vertical slices on the MMCA.Common framework. It exists because standing up a new app by hand meant 12…","i":"MMCA.Templates UseCases dotnet new"},{"u":"/docs/guides/common-TEMPLATES.html#mmca-app","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"mmca-app","x":"The module and aggregate names are independent, so --module Billing --aggregate Invoice is fine. Everything derived from them follows: routes, the Aspire database resource, the…","i":"IntegrationEventContractTestsBase Contoso.Support.Orders.Shared ShipmentLineIdentifierType Zeta.App.Orders.Shared ArchitectureTests.cs builder.AddProject ProjectReference Contoso.Support EditLineRequest RequesterUserId AddLineRequest AppHost.csproj"},{"u":"/docs/guides/common-TEMPLATES.html#mmca-module","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"mmca-module","x":"All six behave exactly as they do for mmca-app, and they are per module: a solution can hold a flat, status-less catalog module beside one whose aggregate owns a growing child…","i":"SQLServerMigrationsAssembly services.AddErrorResources Architecture.Tests.csproj OrderItemIdentifierType Directory.Build.props Migrations.SqlServer Contoso.Support ErrorResources ModuleLoader DataSources FirstModule RemoveItem"},{"u":"/docs/guides/common-TEMPLATES.html#buildadd-moduleps1","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"build/add-module.ps1","x":"Since 1.4.0 every solution mmca-app generates ships this script, and it is the supported way to add a second module. It runs mmca-module with your shape options passed through,…","i":"IntegrationEventContractTests migrations copyOnly dotnet diff Name slnx add git"},{"u":"/docs/guides/common-TEMPLATES.html#mmca-command-and-mmca-query","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"mmca-command and mmca-query","x":"Run these from the module's UseCases folder. Each creates a folder named after the slice holding its two files. --child-collection exists because both handlers load through…","i":"EntityControllerBase MMCA.Templates GetByIdAsync definition CacheKey Comments includes UseCases contain dotnet nameof Result"},{"u":"/docs/guides/common-TEMPLATES.html#how-the-pack-is-built","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"How the pack is built","x":"The template content is the MMCA.Helpdesk reference application itself, staged at pack time. There is no second copy of the solution, so the template cannot drift from the app…"},{"u":"/docs/guides/common-VERSIONING.html","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","x":"MMCA.Common publishes fifteen NuGet packages that are versioned and released together as a single unit. They share one version number so a consumer never has to reason about…","i":"MMCA.Common.UI.Maui release.yml"},{"u":"/docs/guides/common-VERSIONING.html#semantic-versioning","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Semantic Versioning","x":"Versions follow SemVer 2.0: MAJOR.MINOR.PATCH: - MAJOR: reserved (see \"Breaking changes within 1.x\" below). - MINOR: new capability, and the channel breaking changes currently…","i":"vMAJOR.MINOR.PATCH MAJOR.MINOR.PATCH v1.51.0"},{"u":"/docs/guides/common-VERSIONING.html#what-counts-as-breaking","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"What counts as breaking","x":"A change is breaking if it is any of: - Removing or renaming a public type/member, or changing a signature. - Changing the meaning of an existing configuration key, or changing a…","i":"Result"},{"u":"/docs/guides/common-VERSIONING.html#breaking-changes-within-1x","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Breaking changes within 1.x","x":"Breaking changes ship as MINOR bumps, not MAJOR ones, and the version number is therefore not a reliable breakage signal on its own. This is deliberate and follows from the…","i":"IIntegrationEventPublisher IntegrationEventPublisher WithSQLServerDataSource WithDataSource IEventBus v1.123.0 v1.79.0"},{"u":"/docs/guides/common-VERSIONING.html#consumer-rollout","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Consumer rollout","x":"Per project convention, framework upgrades are swept across all consumers in one pass: there are no opt-in flags or phased rollouts for a MMCA.Common change. When a release…"},{"u":"/docs/guides/common-VERSIONING.html#deprecation","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Deprecation","x":"There is no [Obsolete] grace period today. Because the lockstep sweep updates every first-party caller in the same change set, a superseded API is removed in the release that…","i":"Obsolete"},{"u":"/docs/guides/common-VERSIONING.html#supply-chain","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Supply chain","x":"- All package versions are centrally pinned (Directory.Packages.props). - NuGet lock files are committed for reproducible restores. - MassTransit is pinned to v8 by policy (v9…","i":"Directory.Packages.props MassTransit"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","x":"The automated layer (axe-core WCAG 2.1 AA scans in Tests/E2E/MMCA.Store.E2E.Tests/Workflows/AccessibilityTests.cs plus the shared Login/Register/Profile bases in…","i":"MMCA.Common.Testing.E2E"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#tools","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Tools","x":"Run against the app started via Aspire (dotnet run --project Source/Hosting/MMCA.Store.AppHost), reaching the Web UI at https://localhost:6002. Test with the keyboard only (no…","i":"dotnet start Ctrl Alt Cmd run"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#what-to-verify-on-every-flow","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"What to verify on every flow","x":"1. Landmarks and headings. The SR's landmark/heading list (NVDA Insert+F7) exposes a logical banner / navigation / main structure and a sensible h1...hN outline (one h1 per…","i":"Wcag21AaExceptMudPagerCombobox MainLayout.razor MMCA.Common.UI navigation h1...hN banner Insert Shift main Tab"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#results-log","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Results log","x":"Record one row per pass. A flow with any unresolved blocker is a FAIL (open a backlog item for it)."},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#after-a-pass","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"After a pass","x":"- File any defect as a remediation item; cross-link it from the §21 row's evidence. - Update the dated row above so the scorecard can cite a real, current manual pass (this is…"},{"u":"/docs/guides/store-BusinessWorkflows.html","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications"},{"u":"/docs/guides/store-BusinessWorkflows.html#workflow-list-summary","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"Workflow List Summary","x":"---","i":"productId variantId imageId DELETE userId POST GET PUT"},{"u":"/docs/guides/store-BusinessWorkflows.html#1-identity-module-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"1. Identity Module Workflows","x":"Entry Point: POST /auth/register, AuthController.RegisterAsync(), AllowAnonymous Execution Path: Business Steps: 1. Validate registration input (email, password, first name, last…","i":"AuthController.RegisterAsync AuthController.LoginAsync User.RefreshTokenExpiry Customer.ChangeAddress CustomerAddressChanged Customer.ChangeEmail CustomerEmailChanged RequireAuthenticated Customer.ChangeName CustomerNameChanged User.RefreshToken CustomerCreated"},{"u":"/docs/guides/store-BusinessWorkflows.html#2-catalog-module-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"2. Catalog Module Workflows","x":"Entry Point: POST /categories, Admin only, [Idempotent] Response: 201 Created with CategoryDTO Entry Point: PUT /categories/{id}/name, Admin only Entry Point: PUT…","i":"CatalogFeatures.ProductImages ProductVariantPriceChanged ProductVariantCartInfoDTO ProductVariantSkuChanged IProductVariantService ProductVariantRemoved ProductNameChanged ParentCategoryId ProductImageData CategoryDeleted ProductImageDTO ProductDeleted"},{"u":"/docs/guides/store-BusinessWorkflows.html#3-sales-module-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"3. Sales Module Workflows","x":"Entry Point: POST /shoppingcarts/{customerId}/shoppingcartitems, Authenticated (owner or admin via OwnerOrAdminFilter) Decision Points: - Product variant doesn't exist - NotFound…","i":"ShoppingCartItemQuantityAdjusted InventoryItem.AvailableQuantity OrderPaymentFailedSagaHandler BulkSetInventoryResultDTO Order.InventoryRestored ProductVariant.NotFound ShoppingCartItemRemoved IProductVariantService ShoppingCartCheckedOut StripePaymentIntentId ShoppingCart.Status ShoppingCartCleared"},{"u":"/docs/guides/store-BusinessWorkflows.html#4-ui-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"4. UI Workflows","x":"The UI provides a complete shopping experience through the CartDrawer component and Blazor pages. The CartDrawer is the only cart UI: there is no dedicated cart page. It is a…","i":"ICartStateService IUIModule OnChange"},{"u":"/docs/guides/store-BusinessWorkflows.html#5-cross-module-interactions","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"5. Cross-Module Interactions","x":"Module dependency: Sales declares a hard dependency on Catalog (RequiresDependencies = true). When Catalog is disabled, a DisabledProductVariantService stub is registered and…","i":"DisabledProductVariantService IProductVariantService UserRegisteredHandler RequiresDependencies GetUnitPricesAsync GetIdBySkuAsync SkuExistsAsync ExistsAsync true"},{"u":"/docs/guides/store-BusinessWorkflows.html#6-external-interactions","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"6. External Interactions","x":"---","i":"StripePaymentService IDbContextFactory SmtpEmailSender"},{"u":"/docs/guides/store-BusinessWorkflows.html#7-cross-cutting-concerns-participating-in-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"7. Cross-Cutting Concerns Participating in Workflows","x":"---","i":"TransactionalCommandDecorator ProfilingCommandDecorator CachingCommandDecorator ProfilingQueryDecorator ICacheInvalidating OwnerOrAdminFilter IdempotencyFilter ITransactional ApiVersion Idempotent"},{"u":"/docs/guides/store-BusinessWorkflows.html#8-end-to-end-customer-journey","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"8. End-to-End Customer Journey","x":"Alternative Flows: - Payment fails - Order status PaymentFailed - customer can retry (create new Stripe session) - Cancel order - Status Cancelled (from PendingPayment,…","i":"StripePaymentIntentId PaymentFailed Cancelled"},{"u":"/docs/guides/store-BusinessWorkflows.html#9-potentially-missing-or-incomplete-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"9. Potentially Missing or Incomplete Workflows","x":"--- This document is derived from source code analysis. All workflows, decisions, and behaviors described above are confirmed implementations traceable to the referenced source…","i":"OrderPaymentFailedSagaHandler OrderCancelledSagaHandler MarkAsDelivered SmtpEmailSender User.Deactivate UserDeactivated"},{"u":"/docs/guides/store-NavigationFlow.html","d":"Navigation Flow","k":"Guides & Specifications","x":"This document maps the site navigation flow for each actor in the MMCA.Store application. Each mermaid diagram shows the pages accessible to that actor and the directional…","i":"NavigationFlow.md"},{"u":"/docs/guides/store-NavigationFlow.html#actors","d":"Navigation Flow","k":"Guides & Specifications","t":"Actors","x":"Roles and enforcement: Admin is the only elevated role (registration creates a Customer). The 14 admin pages carry page-level [Authorize(Roles = \"Admin\")], regression-gated in CI…","i":"customer_id Authorize Customer Admin Roles"},{"u":"/docs/guides/store-NavigationFlow.html#1-anonymous-user","d":"Navigation Flow","k":"Guides & Specifications","t":"1. Anonymous User","x":"Pages accessible without authentication: home, login, register, the two password-reset pages, and the public catalog. Add-to-cart on the product detail page sits inside an…","i":"AuthorizeView Authorize"},{"u":"/docs/guides/store-NavigationFlow.html#2-customer-authenticated-user","d":"Navigation Flow","k":"Guides & Specifications","t":"2. Customer (Authenticated User)","x":"Inherits all anonymous pages. Gains the profile page, the cart drawer (a layout component, not a route), checkout, and their own orders. Unauthenticated visitors deep-linking to…","i":"OrphanOrderRecovery Specification Authorize"},{"u":"/docs/guides/store-NavigationFlow.html#3-admin","d":"Navigation Flow","k":"Guides & Specifications","t":"3. Admin","x":"Inherits all customer pages, plus the admin CRUD surfaces for all three modules. Every page below carries [Authorize(Roles = \"Admin\")]; a customer deep-linking to any of them…","i":"Authorize Roles"},{"u":"/docs/guides/store-NavigationFlow.html#authorization-model","d":"Navigation Flow","k":"Guides & Specifications","t":"Authorization Model","x":"Three cooperating layers; the API is always the boundary: 1. Page-level route guards. The 14 admin pages carry [Authorize(Roles = \"Admin\")] and /profile / /orders carry…","i":"OwnershipHelper.GetOwnershipSpecification OwnerOrAdminFilter mmca_auth_access AuthorizeView customer_id Authorize c4adff2 Roles"},{"u":"/docs/guides/store-Specification.html","d":"MMCA Business Specification Document","k":"Guides & Specifications"},{"u":"/docs/guides/store-Specification.html#1-system-overview","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"1. System Overview","x":"MMCA is an e-commerce platform built with .NET 10.0 using DDD and Clean Architecture. The business logic is organized as modules (Catalog, Sales, Identity) that have been…"},{"u":"/docs/guides/store-Specification.html#2-core-business-entities","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"2. Core Business Entities","x":"Description: A classification grouping for products. Supports hierarchical (parent-child) structures for nested categorization (e.g., \"Jewelry\" \"Rings\"). Key Properties:…"},{"u":"/docs/guides/store-Specification.html#3-business-workflows","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"3. Business Workflows","x":"Trigger: A new user submits registration with first name, last name, email, and password. Steps: 1. Validate registration request (email format, password requirements) 2. Verify…","i":"IInventoryAllocationService.DecrementAsync CatalogFeatures.ProductImages payment_intent.payment_failed EventUtility.ConstructEvent IProductImageStorageService checkout.session.completed ProductImageStorageService OrderCancelledSagaHandler checkout.session.expired Order.InventoryRestored Auth.InvalidResetToken IProductVariantService"},{"u":"/docs/guides/store-Specification.html#4-order-status-state-machine","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"4. Order Status State Machine","x":"Cancellable States: PendingPayment, PaymentInitiated, PaymentFailed Manual Payment States: PendingPayment, PaymentInitiated, PaymentFailed Terminal States: Cancelled, Delivered ---"},{"u":"/docs/guides/store-Specification.html#5-business-rules","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"5. Business Rules","x":"---","i":"ForgotPasswordRequestValidator ProductVariantConfiguration.cs ResetPasswordRequestValidator InventoryItemInvariants.cs AdjustInventoryHandler.cs PasswordResetTokenService ShoppingCartInvariants.cs CategoryConfiguration.cs CheckOutDomainService.cs CustomerConfiguration.cs ForgotPasswordHandler.cs UserRegisteredHandler.cs"},{"u":"/docs/guides/store-Specification.html#6-use-cases","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"6. Use Cases","x":"---"},{"u":"/docs/guides/store-Specification.html#7-domain-events-and-state-changes","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"7. Domain Events and State Changes","x":"---"},{"u":"/docs/guides/store-Specification.html#8-external-integrations","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"8. External Integrations","x":"Purpose: Processes online customer payments for orders. Business Impact: Enables the system to collect payments from customers and confirm payment success or failure…","i":"OrderPaymentFailedSagaHandler payment_intent.payment_failed EventUtility.ConstructEvent checkout.session.completed checkout.session.expired OrderPaidHandler Result.Failure StripeSettings WebhookSecret IEmailSender SecretKey"},{"u":"/docs/guides/store-Specification.html#9-authorization-model","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"9. Authorization Model","x":"Ownership Enforcement: The OwnerOrAdminFilter validates that the route parameter id (CustomerIdentifierType) matches the authenticated user's customer ID, or that the user has…","i":"OwnerOrAdminFilter customer_id user_id email POST role iat jti sub"},{"u":"/docs/guides/store-Specification.html#10-cross-module-communication","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"10. Cross-Module Communication","x":"The system enforces strict module boundaries. Modules communicate only through shared interface contracts: Confirmed behaviors: - Sales module cannot directly access Catalog…","i":"DisabledProductVariantService IProductVariantService RequiresDependencies true"},{"u":"/docs/guides/store-Specification.html#11-user-interface","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"11. User Interface","x":"The UI is a Blazor Server + WebAssembly hybrid (InteractiveAuto render mode) using MudBlazor component library. It supports multiple hosting targets: - Web (Server + WASM):…","i":"UIModuleConfiguration.IsModuleEnabled ICartStateService InteractiveAuto configuration moduleName IUIModule Assembly NavItems"},{"u":"/docs/guides/store-Specification.html#12-cross-cutting-infrastructure","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"12. Cross-Cutting Infrastructure","x":"The IdempotencyFilter (applied via [Idempotent] attribute on Create endpoints) caches the first response for a given Idempotency-Key header value for 24 hours. Duplicate requests…","i":"TransactionalCommandDecorator ProfilingCommandDecorator CachingCommandDecorator ProfilingQueryDecorator ICacheInvalidating IDataSourceService IDbContextFactory IdempotencyFilter ITransactional SemaphoreSlim UseDataSource Idempotent"},{"u":"/docs/guides/store-Specification.html#13-testing","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"13. Testing","x":"- Full customer journey: Register - Browse - Add to Cart - Checkout - Admin Pay - Deliver - Order lifecycle: all state transitions including cancellation with inventory…","i":"MMCA.Store.Integration.slnf MMCA.Store.IntegrationTests WebApplicationFactory STORE_TEST_SQL_BASE"},{"u":"/docs/guides/store-Specification.html#14-missing-or-unclear-business-logic","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"14. Missing or Unclear Business Logic","x":"Observation: The SMTP email service infrastructure is implemented, but no domain event handlers trigger email notifications for events like order confirmation, payment receipt,…","i":"InventoryItemsController InventoryItemList MarkAsDelivered User.Deactivate UserDeactivated CategoryId Delivered GetPaged GetById GetAll Lookup Paid"},{"u":"/docs/guides/store-Specification.html#15-seed-data-initial-system-state","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"15. Seed Data (Initial System State)","x":"The system seeds the following data at startup: Users: - Admin: one seeded administrator account (Admin role, no Customer record; credentials are environment-specific and not…","i":"ExistsAsync"},{"u":"/","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"Senior Software Architect Ivan Ball-llovera Cloud-native enterprise architecture on the Microsoft stack I design and ship production-grade .NET platforms: modular monoliths that…"},{"u":"/","t":"Architecture that earns its keep","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"I am a Senior Software Architect with more than 25 years designing and delivering scalable, cloud-native systems on the Microsoft stack. My focus is Domain-Driven Design, Clean…"},{"u":"/","t":"The MMCA platform","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"A production-grade .NET 10 framework and a set of reference apps that demonstrate modern enterprise architecture end to end. It is built as a modular monolith that extracts…"},{"u":"/","t":"Deep dives on enterprise .NET","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"A long-form series turning the framework's decisions into teachable patterns, every claim grounded in real source. The three most recent: Proof & getting started · No. 50 The…"},{"u":"/","t":"Speaking & giving back","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"I help run two community-driven Atlanta technology conferences and keep production-grade patterns free and in the open. See talks & community work Organizer & speaker Two Atlanta…"},{"u":"/resume.html","d":"Résumé","k":"Site","x":"Résumé Ivan Ball-llovera Senior Software Architect 25+ years designing and delivering scalable, cloud-native systems on the Microsoft stack: Domain-Driven Design, Clean…"},{"u":"/resume.html","t":"Professional summary","d":"Résumé","k":"Site","x":"Senior Software Architect with 25+ years designing and delivering scalable, cloud-native systems on the Microsoft stack. Deep expertise in Domain-Driven Design, Clean…"},{"u":"/resume.html","t":"Core competencies","d":"Résumé","k":"Site","x":"Architecture & design Domain-Driven Design, Clean Architecture, CQRS, Modular Monolith → Microservices, Event-Driven Architecture, Outbox Pattern, gRPC, API Gateway (YARP),…"},{"u":"/resume.html","t":"Professional experience","d":"Résumé","k":"Site","x":"Senior Software Engineer · Assurant June 2025 – Present · Architect-level scope: platform, security, and cross-team technical decisions Re-architected the AR.com renters quote…"},{"u":"/resume.html","t":"Featured project · MMCA platform","d":"Résumé","k":"Site","x":"Personal / open source · github.com/ivanball/MMCA.Common A production-grade .NET 10 reference platform demonstrating modern enterprise architecture end-to-end. The conference…"},{"u":"/resume.html","t":"Education","d":"Résumé","k":"Site","x":"B.S., Computer Science University of Havana (Faculty of Mathematics), Havana, Cuba (1994 – 1999)"},{"u":"/resume.html","t":"Languages","d":"Résumé","k":"Site","x":"English · Spanish (bilingual)"},{"u":"/resume.html","t":"Certifications","d":"Résumé","k":"Site","x":"✓ Azure Administrator Associate (AZ-104, 2025) ✓ Azure AI Fundamentals (AI-900, 2024) ✓ Azure Data Fundamentals (DP-900, 2021) ✓ Azure Fundamentals (AZ-900, 2021) → In progress…"},{"u":"/resume.html","t":"Professional development","d":"Résumé","k":"Site","x":"Continuously prototypes emerging technologies, with a current focus on Clean Architecture using .NET 10, Blazor, .NET MAUI, and ASP.NET Core Web API, and on AI-assisted…"},{"u":"/platform.html","d":"The MMCA Platform","k":"Site","x":"Featured work · Open source The MMCA platform A production-grade .NET 10 platform that demonstrates modern enterprise architecture end to end: an open-source framework of fifteen…"},{"u":"/platform.html","t":"MMCA.Common","d":"The MMCA Platform","k":"Site","x":"A NuGet package framework for building modular-monolith applications with DDD, Clean Architecture, and CQRS, and the extension points to extract a module into its own…"},{"u":"/platform.html","t":"Three reference applications","d":"The MMCA Platform","k":"Site","x":"The framework is consumed by real apps. Each one exercises the patterns differently, and the conference app ran a live event on Azure. Conference MMCA.ADC A production-deployed…"},{"u":"/platform.html","t":"From one graph, laptop to cloud","d":"The MMCA Platform","k":"Site","x":"Orchestrated locally with .NET Aspire, then deployed to Azure Container Apps from the same declarative model. The .NET Aspire dashboard: services, databases, and the broker as…"},{"u":"/platform.html","t":"Architectural styles the codebase commits to","d":"The MMCA Platform","k":"Site","x":"The recurring ideas every repo is built on, summarized from the onboarding primer's orientation chapter. Each is taught in full at its first concrete appearance in the reference…"},{"u":"/platform.html","t":"A two-axis architecture scorecard","d":"The MMCA Platform","k":"Site","x":"Every repo is scored against a 34-category rubric on two axes, Maturity (how complete the decision is) and Implementation (how well it is realized in code), with evidence and…"},{"u":"/platform.html","t":"Architecture Decision Records","d":"The MMCA Platform","k":"Site","x":"91 ADRs capture the context and trade-offs behind each cross-cutting pattern, so the design is teachable, not tribal knowledge. Every entry below links to the full record. 001…"},{"u":"/platform.html","t":"The reference library","d":"The MMCA Platform","k":"Site","x":"The full architecture documentation, maintained in this site's repository as its canonical home and rendered as browsable pages: every Architecture Decision Record, the…"},{"u":"/platform.html","t":"Use it, read it, or follow along","d":"The MMCA Platform","k":"Site","x":"The framework is Apache 2.0 and published to nuget.org. The fastest way in is the reference app: one module wired end to end through all five layers, with the extraction path…"},{"u":"/platform.html","t":"Get each deep dive by email","d":"The MMCA Platform","k":"Site","x":"One message per article, no digests and no other mail. Email address Subscribe"},{"u":"/writing.html","d":"Writing","k":"Site","x":"Writing Deep dives on enterprise .NET A long-form series that turns the MMCA framework's architecture decisions into teachable patterns, every claim grounded in real source. Read…"},{"u":"/writing.html","t":"Get each deep dive by email","d":"Writing","k":"Site","x":"One message per article, no digests and no other mail. Email address Subscribe"},{"u":"/speaking.html","d":"Speaking & Community","k":"Site","x":"Speaking & community Talks and giving back For more than 20 years I have been an active contributor to the Microsoft developer communities in Atlanta and South Florida: teaching,…"},{"u":"/speaking.html","t":"Recent sessions","d":"Speaking & Community","k":"Site","x":"Atlanta Cloud + AI Conference · 2026 The App You're Using Right Now Building Atlanta Cloud + AI's own platform with Claude in the loop A field report, not a slide deck about…"},{"u":"/speaking.html","t":"Organizing two Atlanta conferences","d":"Speaking & Community","k":"Site","x":"I help convene developers in person, giving the local community direct, no-cost access to expert content on the Microsoft platform. Lead organizer Atlanta Cloud + AI Conference…"},{"u":"/speaking.html","t":"User groups","d":"Speaking & Community","k":"Site","x":"An active participant in Atlanta's Microsoft technology user-group ecosystem, the same community network from which the conferences draw their speakers and attendees. • Atlanta…"},{"u":"/speaking.html","t":"Open source & mentorship","d":"Speaking & Community","k":"Site","x":"My MMCA framework is Apache-2.0 licensed and documented with architecture decision records, so the patterns are not just usable but teachable. I mentor developers one on one,…"},{"u":"/speaking.html","t":"What I speak on","d":"Speaking & Community","k":"Site","x":"Sessions and workshops for conferences, user groups, and teams. Clean Architecture & DDD on .NET Modular monolith → microservices The transactional outbox Database-per-service…"},{"u":"/contact.html","d":"Contact","k":"Site","x":"Contact Let's connect Happy to talk architecture, the MMCA platform, speaking at your conference or user group, or comparing notes on .NET and Azure. The fastest ways to reach…"},{"u":"/contact.html","t":"Three places to start","d":"Contact","k":"Site","x":"Open source The MMCA platform A .NET 10 framework and three reference apps, graded in the open against a 34-category rubric. See the architecture → Writing Deep dives on…"},{"u":"https://medium.com/@ivanball76/the-mmca-series-every-pattern-one-place-28cf2cee7be8","t":"The series index","d":"Article no. 50","k":"Proof & getting started","x":"The full series index and recommended reading order.","e":1},{"u":"https://medium.com/@ivanball76/undo-is-a-feature-saga-compensation-and-the-reconciliation-backstop-fa017f9591b8","t":"Saga compensation and the reconciliation backstop","d":"Article no. 49","k":"Core patterns","x":"Undo as a first-class event handler: give back stock a committed transaction already took, with a periodic sweep as the saga-timeout backstop.","e":1},{"u":"https://medium.com/@ivanball76/observability-by-default-opentelemetry-and-azure-monitor-in-mmca-673c1886e9e0","t":"Observability by default","d":"Article no. 48","k":"Run & extract","x":"A shared OpenTelemetry baseline with CQRS duration metrics, correlation IDs, and outbox-poll span filtering, exported to Azure Monitor.","e":1},{"u":"https://medium.com/@ivanball76/security-headers-and-csp-for-blazor-one-middleware-every-host-af82df95236e","t":"Security headers and CSP for Blazor","d":"Article no. 47","k":"Auth & the edge","x":"One middleware stamps hardened response headers on every host, with the Blazor CSP resolved through a pluggable provider.","e":1},{"u":"https://medium.com/@ivanball76/field-level-encryption-in-ef-core-aes-gcm-for-pii-columns-06ece340ea25","t":"Field-level encryption in EF Core","d":"Article no. 46","k":"Data & persistence","x":"An AES-256-GCM value converter that keeps a PII column ciphertext even for someone who can query the database.","e":1},{"u":"https://medium.com/@ivanball76/feature-flags-in-the-cqrs-pipeline-gate-commands-not-code-e58b9ea8d098","t":"Feature flags in the CQRS pipeline","d":"Article no. 45","k":"Core patterns","x":"Gate commands and queries at the outermost decorator, so a handler never checks a flag and a disabled feature is rejected before any work runs.","e":1},{"u":"https://medium.com/@ivanball76/http-api-versioning-proven-not-just-claimed-2b0381e4b533","t":"HTTP API versioning, proven not just claimed","d":"Article no. 44","k":"Auth & the edge","x":"Header-based versioning introduced without breaking a single caller, plus a fitness contract that proves two live versions coexist.","e":1},{"u":"https://medium.com/@ivanball76/managed-file-storage-uploads-you-dont-have-to-trust-8dfe8bf016bc","t":"Managed file storage: uploads you don't have to trust","d":"Article no. 43","k":"Data & persistence","x":"Attacker-controlled bytes become safe avatars: content sniffing, metadata stripping, re-encoding, and pluggable blob storage.","e":1},{"u":"https://medium.com/@ivanball76/one-blazor-ui-two-hosts-a-device-capability-layer-that-stays-resolvable-everywhere-b85444693161","t":"One Blazor UI, two hosts","d":"Article no. 42","k":"Proof & getting started","x":"The same Blazor components run in a browser and inside a MAUI hybrid app; small per-capability contracts reach native hardware without ever asking 'am I on mobile?'.","e":1},{"u":"https://medium.com/@ivanball76/two-real-apps-on-one-framework-a-conference-platform-and-a-store-12f694d2a361","t":"Two real apps on one framework","d":"Article no. 41","k":"Proof & getting started","x":"A case study: a conference platform and an e-commerce store built on the same kernel.","e":1},{"u":"https://medium.com/@ivanball76/write-your-first-architecture-fitness-test-d4e25e6a4741","t":"Write your first fitness test","d":"Article no. 40","k":"Proof & getting started","x":"Author your first architecture fitness test and watch it fail the build on a violation.","e":1},{"u":"https://medium.com/@ivanball76/scaffold-a-net-modular-monolith-in-one-command-then-build-your-first-module-b10aacd16d33","t":"Build your first module","d":"Article no. 39","k":"Proof & getting started","x":"A hands-on walkthrough of building a new module across all five layers.","e":1},{"u":"https://medium.com/@ivanball76/one-preference-two-switches-shipping-i18n-and-dark-mode-on-a-single-cookie-and-profile-pipeline-f97186038909","t":"i18n and theming on one preference pipeline","d":"Article no. 38","k":"Proof & getting started","x":"A culture choice and a theme choice ride the same cookie, profile column, and login reconciliation: one persistence path, two switches.","e":1},{"u":"https://medium.com/@ivanball76/a-list-page-in-a-few-lines-a-reusable-blazor-ui-framework-with-the-same-discipline-as-the-backend-c66fa16cd561","t":"A reusable Blazor UI framework","d":"Article no. 37","k":"Proof & getting started","x":"A shared Blazor and MudBlazor UI layer with accessibility enforced by axe in CI.","e":1},{"u":"https://medium.com/@ivanball76/soft-delete-vs-the-right-to-erasure-the-gdpr-conflict-and-the-erasure-pathway-e1d350007509","t":"Soft-delete vs the right to erasure","d":"Article no. 36","k":"Proof & getting started","x":"Soft-delete for lifecycle, anonymization plus outbox purge for GDPR/CCPA erasure, and why both exist.","e":1},{"u":"https://medium.com/@ivanball76/the-test-pyramid-not-the-ice-cream-cone-1-880-fast-tests-zero-docker-cb459fda73d9","t":"The test pyramid","d":"Article no. 35","k":"Proof & getting started","x":"How the framework's tests stack up: fast unit and architecture tests at the base, E2E at the tip.","e":1},{"u":"https://medium.com/@ivanball76/architecture-fitness-functions-rules-that-fail-the-build-not-a-wiki-page-6562940deceb","t":"Architecture fitness functions","d":"Article no. 34","k":"Proof & getting started","x":"Architecture rules that fail the build: a compile-time layer guard plus a shared NetArchTest rule library.","e":1},{"u":"https://medium.com/@ivanball76/retries-are-not-a-recovery-plan-resilience-handlers-rto-rpo-and-a-restore-you-actually-drilled-3c7474814123","t":"Resilience and recovery objectives","d":"Article no. 33","k":"Run & extract","x":"Standard resilience on every outbound client, plus declared RTO/RPO and a drilled restore.","e":1},{"u":"https://medium.com/@ivanball76/extracting-a-module-to-a-grpc-service-live-799926cf8a32","t":"Extracting a module to a gRPC service","d":"Article no. 32","k":"Run & extract","x":"A step-by-step extraction of an in-process module into its own gRPC service, database, and auth.","e":1},{"u":"https://medium.com/@ivanball76/aspire-one-command-brings-up-the-whole-distributed-app-379b5cffdeed","t":"Aspire: one command","d":"Article no. 31","k":"Run & extract","x":"Model services, databases, and the broker as one Aspire graph that runs from laptop to Azure with one command.","e":1},{"u":"https://medium.com/@ivanball76/defending-the-api-edge-three-controls-that-cover-the-whole-surface-d958ecae1091","t":"Rate limiting and brute-force protection","d":"Article no. 30","k":"Auth & the edge","x":"Two layers that cover the whole API edge: endpoint rate limits plus lockout-based brute-force defense on identity.","e":1},{"u":"https://medium.com/@ivanball76/resource-ownership-authorization-which-rows-you-may-touch-not-just-which-actions-cb8e78867bae","t":"Resource-ownership authorization","d":"Article no. 29","k":"Auth & the edge","x":"Beyond roles and permissions: which rows you may touch, enforced per resource.","e":1},{"u":"https://medium.com/@ivanball76/generic-entity-controllers-and-the-dynamic-query-contract-adr-034-2b5c799bc69f","t":"Generic entity controllers","d":"Article no. 28","k":"Auth & the edge","x":"A write-once REST surface every entity inherits, plus a bounded dynamic query contract that is never open SQL.","e":1},{"u":"https://medium.com/@ivanball76/one-rotating-refresh-token-and-reuse-detection-that-makes-theft-self-limiting-fab42234a04a","t":"One rotating refresh token","d":"Article no. 27","k":"Auth & the edge","x":"A short-lived JWT plus one server-stored refresh token that rotates on every use, with reuse detection that makes a stolen token end its own session.","e":1},{"u":"https://medium.com/@ivanball76/google-and-github-login-without-leaking-tokens-external-oauth-behind-your-own-jwts-d68ba5e3aca4","t":"External OAuth login behind your own JWTs","d":"Article no. 26","k":"Auth & the edge","x":"Sign in with Google or GitHub without leaking provider tokens: external identity exchanged for your own JWTs at the boundary.","e":1},{"u":"https://medium.com/@ivanball76/browser-session-cookie-auth-for-blazor-ssr-surviving-the-f5-eb0ea317820e","t":"Browser session-cookie auth for Blazor SSR","d":"Article no. 25","k":"Auth & the edge","x":"HttpOnly session cookies and an SSR-time scheme so [Authorize] passes during prerender, with the API still the boundary.","e":1},{"u":"https://medium.com/@ivanball76/permission-based-authorization-capabilities-over-role-checks-ea6574cbee27","t":"Permission-based authorization over roles","d":"Article no. 24","k":"Auth & the edge","x":"A capability layer over RBAC: permission policies that resolve on demand from a central registry.","e":1},{"u":"https://medium.com/@ivanball76/delete-automapper-explicit-compile-time-dto-mapping-that-you-can-actually-test-9c7013cc5d3f","t":"Delete AutoMapper: manual DTO mapping","d":"Article no. 23","k":"Auth & the edge","x":"Why source-generated, per-entity mappers beat reflection-based mapping for clarity and speed.","e":1},{"u":"https://medium.com/@ivanball76/ephemeral-by-design-sub-second-live-channels-over-one-signalr-hub-0248050e0c8b","t":"Live channels over one SignalR hub","d":"Article no. 22","k":"Auth & the edge","x":"Sub-second ephemeral events (polls, Q&A, live counts) fanned out over the existing notification hub, with nothing persisted.","e":1},{"u":"https://medium.com/@ivanball76/notifications-as-a-vertical-slice-in-app-inbox-real-time-push-native-push-and-email-c59d5a4f3b69","t":"Notifications as a vertical slice","d":"Article no. 21","k":"Auth & the edge","x":"A notifications feature built as a clean vertical slice across every layer.","e":1},{"u":"https://medium.com/@ivanball76/problem-details-across-http-and-grpc-rfc-9457-9f20157cf7de","t":"Problem Details across HTTP and gRPC","d":"Article no. 20","k":"Auth & the edge","x":"One error contract mapped consistently to HTTP Problem Details and gRPC status.","e":1},{"u":"https://medium.com/@ivanball76/the-self-invalidating-cache-that-lives-in-the-pipeline-not-your-handlers-e11548062d2f","t":"The self-invalidating cache","d":"Article no. 19","k":"Auth & the edge","x":"A caching decorator where commands invalidate and queries populate, plus an authenticated output-cache tier at the API edge.","e":1},{"u":"https://medium.com/@ivanball76/idempotency-in-one-attribute-safe-retries-for-http-apis-065848fd03f4","t":"Idempotency in one attribute","d":"Article no. 18","k":"Auth & the edge","x":"Dedup client retries with an Idempotency-Key header and cached replay, plus a consumer-side inbox for brokers.","e":1},{"u":"https://medium.com/@ivanball76/password-hashing-done-right-pbkdf2-sha512-600k-iterations-timing-safe-d64ddb802403","t":"Password hashing done right","d":"Article no. 17","k":"Auth & the edge","x":"The non-negotiables of password storage in .NET, done correctly and tested.","e":1},{"u":"https://medium.com/@ivanball76/cross-service-auth-without-a-shared-secret-jwks-dual-fetch-478e6f688c7e","t":"JWKS cross-service auth","d":"Article no. 16","k":"Auth & the edge","x":"Validate another service's RS256 tokens via JWKS discovery, with no shared secret crossing a boundary.","e":1},{"u":"https://medium.com/@ivanball76/event-schema-versioning-never-silently-reshape-an-event-93cd5d4a156d","t":"Event-schema versioning","d":"Article no. 15","k":"Data & persistence","x":"Every integration event carries a schema version; breaking changes get a new event type and an upcaster, never a silent reshape.","e":1},{"u":"https://medium.com/@ivanball76/self-ordering-modules-discovered-kahn-ordered-and-extractable-2ce7283a26b5","t":"Self-ordering modules","d":"Article no. 14","k":"Data & persistence","x":"Modules declare their dependencies and load in topological order, so registration is never hand-sequenced.","e":1},{"u":"https://medium.com/@ivanball76/optimistic-concurrency-that-survives-the-round-trip-rowversion-from-database-to-dto-and-back-93d4a794716f","t":"Optimistic concurrency: RowVersion round-trips","d":"Article no. 13","k":"Data & persistence","x":"Carry the RowVersion from database to DTO and back, so a concurrent edit fails fast as a conflict instead of silently overwriting.","e":1},{"u":"https://medium.com/@ivanball76/ef-core-include-chains-are-a-trap-navigation-populators-decouple-eager-loading-c378fa4497ac","t":"Navigation populators","d":"Article no. 12","k":"Data & persistence","x":"Eager-load relationships that cross containers and data sources without N+1 or a leaky abstraction.","e":1},{"u":"https://medium.com/@ivanball76/one-entity-model-three-databases-polyglot-persistence-behind-a-single-attribute-760e77974d5d","t":"Polyglot persistence: one model, three engines","d":"Article no. 11","k":"Data & persistence","x":"SQL Server, Cosmos, and SQLite behind a single entity model, with the engine chosen by attribute.","e":1},{"u":"https://medium.com/@ivanball76/database-per-service-inside-a-monolith-and-why-265092eb03f1","t":"Database-per-service inside a monolith","d":"Article no. 10","k":"Data & persistence","x":"Give each module its own database and outbox before you extract it, so extraction changes hosting, not data.","e":1},{"u":"https://medium.com/@ivanball76/the-transactional-outbox-in-net-10-never-lose-an-event-again-f5a9b7a89e51","t":"The transactional outbox","d":"Article no. 9","k":"Core patterns","x":"Events that survive a crash: persist them atomically with your data, then dispatch at least once.","e":1},{"u":"https://medium.com/@ivanball76/compose-validators-dont-copy-them-a-reusable-fluentvalidation-kit-8865a6003a9c","t":"Compose validators, don't copy them","d":"Article no. 8","k":"Core patterns","x":"A validation kit that composes FluentValidation rules instead of copy-pasting them across features.","e":1},{"u":"https://medium.com/@ivanball76/the-cqrs-decorator-pipeline-logging-caching-and-transactions-without-touching-a-handler-fb7679b8bde8","t":"The CQRS decorator pipeline","d":"Article no. 7","k":"Core patterns","x":"Thin command and query handlers wrapped by a Scrutor decorator chain whose order is load-bearing.","e":1},{"u":"https://medium.com/@ivanball76/specifications-over-linq-spaghetti-composable-reusable-query-intent-8a40dafcbd3d","t":"Specifications over LINQ spaghetti","d":"Article no. 6","k":"Core patterns","x":"Compose queries from reusable specification objects instead of scattering LINQ across handlers.","e":1},{"u":"https://medium.com/@ivanball76/kill-the-anemic-domain-model-rich-aggregates-with-factory-methods-that-return-result-44f2e3d89794","t":"Kill the anemic domain model","d":"Article no. 5","k":"Core patterns","x":"Push behavior into rich aggregates with factory methods and invariants instead of bags of public setters.","e":1},{"u":"https://medium.com/@ivanball76/stop-throwing-exceptions-for-control-flow-the-result-railway-in-c-7a02050b554e","t":"The Result railway in C#","d":"Article no. 4","k":"Core patterns","x":"Model expected failures as Result values with a transport-agnostic error type, and keep exceptions for the genuinely exceptional.","e":1},{"u":"https://medium.com/@ivanball76/what-good-architecture-actually-means-a-34-category-rubric-you-can-score-yourself-against-4002291a6b6a","t":"The 34-category architecture rubric","d":"Article no. 3","k":"Orientation","x":"A two-axis rubric for scoring architecture on maturity and implementation, so 'good architecture' stops being a vibe.","e":1},{"u":"https://medium.com/@ivanball76/modular-monolith-to-microservices-without-the-rewrite-8c3603614f12","t":"Modular monolith to microservices","d":"Article no. 2","k":"Orientation","x":"The cornerstone idea: build the monolith now and extract a service later with no rewrite, via module discovery, gRPC contracts, and a YARP gateway.","e":1},{"u":"https://medium.com/@ivanball76/i-open-sourced-the-enterprise-net-77f9200f3728","t":"Open-sourced and graded against 34 categories","d":"Article no. 1","k":"Orientation","x":"Why I open-sourced a production .NET framework and scored it against a 34-category architecture rubric, gaps and all.","e":1}]} \ No newline at end of file +{"v":1,"n":1340,"r":[{"u":"/docs/adr/index.html","d":"Architecture Decision Records","k":"Architecture Decision Records","x":"Accepted ADRs explaining why the core cross-cutting patterns exist. Read these before changing a pattern they describe: they capture context and trade-offs that aren't obvious…","i":"ApplicationLayer_DoesNotUseRawQueryableSurfaces AsyncMethodsDeclareTrailingCancellationToken MessageBusSettings.EnableDelayedRedelivery PushNotificationSettings.ChannelKeyPattern ApplicationDbContext.ConfigureConventions Microsoft.CodeAnalysis.PublicApiAnalyzers RegisterUpcastedIntegrationEventConsumer ApiParameterDescriptorBackfillProvider IWriteRepository.SetOriginalRowVersion ServiceInfoVersioningContractTestsBase ApplicationDbContext.OnModelCreating MMCA.Common.LayerEnforcement.targets"},{"u":"/docs/adr/index.html#writing-a-new-adr","d":"Architecture Decision Records","k":"Architecture Decision Records","t":"Writing a new ADR","x":"Copy the structure of an existing record: Status (Proposed / Accepted / Superseded, date and link when superseding), Context (the forces and the problem), Decision (what we…"},{"u":"/docs/adr/001-manual-dto-mapping.html","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records"},{"u":"/docs/adr/001-manual-dto-mapping.html#status","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Status","x":"Accepted. Mechanism clarified 2026-06-26: the per-entity mappers are Riok.Mapperly source-generated (compile-time), not hand-written line by line. The decision to avoid runtime…"},{"u":"/docs/adr/001-manual-dto-mapping.html#context","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Context","x":"Domain entities must be mapped to DTOs for API responses. The two common approaches are: 1. Manual mapping classes (IEntityDTOMapper ) 2. Convention-based reflection mapping…","i":"IEntityDTOMapper TEntity TDTO TId"},{"u":"/docs/adr/001-manual-dto-mapping.html#decision","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Decision","x":"Use explicit, per-entity DTO mappers (each a Riok.Mapperly [Mapper] partial class whose MapToDTO body is source-generated at compile time) registered via Scrutor assembly…","i":"IEntityRequestMapper IEntityDTOMapper SpeakerDTOMapper TIdentifierType TCreateRequest UserMapping TEntityDTO MapToDTOs UseMapper MapToDTO partial TEntity"},{"u":"/docs/adr/001-manual-dto-mapping.html#rationale","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Rationale","x":"- Compile-time safety: Mapping errors surface at build time, not runtime. Property renames break the build rather than silently mapping null. - Testability: Each mapper is a…","i":"SpeakerDTOMapper MapToDTO null"},{"u":"/docs/adr/001-manual-dto-mapping.html#trade-offs","d":"ADR-001: Manual DTO Mapping over AutoMapper","k":"Architecture Decision Records","t":"Trade-offs","x":"- More files (31 DTO mappers across Store + ADC: 20 in ADC, 11 in Store, plus the parallel IEntityRequestMapper classes). The interface's default MapToDTOs implementation is…","i":"IEntityRequestMapper MapToDTOs"},{"u":"/docs/adr/002-navigation-populators.html","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records"},{"u":"/docs/adr/002-navigation-populators.html#status","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Status","x":"Accepted"},{"u":"/docs/adr/002-navigation-populators.html#context","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Context","x":"The application supports multiple database backends (SQL Server, Cosmos DB, SQLite). EF Core's .Include() works for SQL Server but fails for Cosmos DB cross-container…","i":"IDataSourceService.HaveIncludeSupport NavigationMetadataProvider declaringType IsCollection Navigation targetType Include"},{"u":"/docs/adr/002-navigation-populators.html#decision","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Decision","x":"Each entity that has unsupported navigations gets a INavigationPopulator implementation. A DeclarativeNavigationPopulator base class (added in MMCA.Common) allows populators to…","i":"DeclarativeNavigationPopulator ChildNavigationDescriptor FKNavigationDescriptor INavigationDescriptor INavigationPopulator NavigationLoader Product.Category Event.Rooms TEntity WHERE"},{"u":"/docs/adr/002-navigation-populators.html#rationale","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Rationale","x":"- Multi-DB support: The query pipeline automatically falls back from Include to NavigationPopulator when the data source reports navigations as unsupported. - Batch efficiency:…","i":"DeclarativeNavigationPopulator"},{"u":"/docs/adr/002-navigation-populators.html#trade-offs","d":"ADR-002: NavigationPopulators for Cross-Container Loading","k":"Architecture Decision Records","t":"Trade-offs","x":"- Extra abstraction layer for SQL Server (where Include works fine). Mitigated: the populator is only called when the query pipeline's metadata says navigations are unsupported.…","i":"NullNavigationPopulator"},{"u":"/docs/adr/003-outbox-dual-dispatch.html","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#status","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-07-19 (integration-event routing via IMessageBus, lease-based claims for safe scale-out, dead-letter visibility, post-commit dispatch; see Revision below).…","i":"IMessageBus"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#context","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Context","x":"Domain events must be reliably published after aggregate changes are persisted. Two failure modes exist: 1. In-process dispatch fails (e.g., handler throws): the event is lost if…"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#decision","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Decision","x":"Use a dual-dispatch strategy: 1. Outbox persistence: Domain events are serialized into OutboxMessage rows within the same database transaction as the aggregate changes. This…","i":"DomainEventDispatcher BackgroundService SaveChangesAsync OutboxProcessor OutboxMessage"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#rationale","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Rationale","x":"- Guaranteed delivery: The outbox table is written atomically with the aggregate changes. Even if the process crashes after persistence, the background processor catches up. -…","i":"OutboxPollFilterProcessor ProcessingDelaySeconds BrokerMessageBus OutboxProcessor BrokerEventBus IMessageBus OutboxPoll"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#trade-offs","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Trade-offs","x":"- Domain event handlers must be idempotent (this is a good practice regardless). - The outbox table grows until processed entries are cleaned up: OutboxCleanupService purges rows…","i":"OutboxCleanupService HasMoreEligibleWork ProcessedOn MaxRetries RetryCount"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-07-19","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-07-19)","x":"Four changes from the 2026-07-19 full review: 1. Integration events route through the outbox to IMessageBus, never local dispatch. An IIntegrationEvent raised via AddDomainEvent…","i":"DomainEventSaveChangesInterceptor outbox.dead_letter.count OutboxCleanupService ExecuteUpdateAsync IIntegrationEvent type_unresolvable integrationEvent OutboxProcessor AddDomainEvent OutboxMessage IMessageBus LockedUntil"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-07-24","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Three capture-side corrections found in a code review. None change the dual-dispatch decision; they close gaps between what it promised and what the interceptor did. 1. Capture…","i":"ExecuteInTransactionAsync RemoveDomainEvents IAggregateRoot SavingChanges RetryCount DbContext LastError catch"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-08-01","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"One retry-pacing correction. The dual-dispatch decision is unchanged; the Trade-offs above described a cadence the processor no longer has. 1. Retry backoff is explicit, and it…","i":"RetryBackoffBaseSeconds"},{"u":"/docs/adr/003-outbox-dual-dispatch.html#revision-2026-08-07","d":"ADR-003: Outbox Pattern with Dual Dispatch","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"One retry-pacing refinement. The decision and the curve are unchanged; the waits are no longer identical across a batch. 1. The retry backoff carries random jitter. The…"},{"u":"/docs/adr/004-authentication-dual-fetch.html","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records"},{"u":"/docs/adr/004-authentication-dual-fetch.html#status","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Status","x":"Accepted."},{"u":"/docs/adr/004-authentication-dual-fetch.html#context","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Context","x":"When the modular monolith is extracted into per-module service hosts behind a gateway (ADR-008), every service must authenticate the same end-user JWT, but only one service…"},{"u":"/docs/adr/004-authentication-dual-fetch.html#decision","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Decision","x":"Validate cross-service tokens with asymmetric (RS256) signatures plus JWKS / OIDC discovery, keeping the symmetric (HS256) shared-secret path as the in-process monolith default.…","i":"TokenValidationParameters.ValidAlgorithms id_token_signing_alg_values_supported OpenIdConnectMetadataWarmupTask JwtSettings.SigningAlgorithm BuildValidationParameters MapOidcDiscoveryEndpoint response_types_supported AddCommonAuthentication subject_types_supported AddForwardedJwtBearer WithJwksDiscovery RsaPublicKeyPath"},{"u":"/docs/adr/004-authentication-dual-fetch.html#rationale","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Rationale","x":"- No shared signing key. Only Identity can mint tokens; every other service holds only the public key it fetched, so a compromised non-Identity service cannot forge tokens, and…"},{"u":"/docs/adr/004-authentication-dual-fetch.html#trade-offs","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Trade-offs","x":"- More moving parts than a shared secret. RS256 needs key generation, distribution of the public half, a JWKS endpoint, and discovery wiring, versus one symmetric string. -…"},{"u":"/docs/adr/004-authentication-dual-fetch.html#related","d":"ADR-004: Cross-Service Token Validation via JWKS / OIDC Discovery","k":"Architecture Decision Records","t":"Related","x":"ADR-007 (gRPC calls forward the validated JWT downstream via JwtForwardingClientInterceptor), ADR-008 (the extraction that split issuer and validator into separate processes),…","i":"JwtForwardingClientInterceptor"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#status","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Status","x":"Accepted. Updated 2026-06-27 (documented the [Pii] → IAnonymizable build-time guard and PiiRedactor).","i":"IAnonymizable PiiRedactor Pii"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#context","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Context","x":"The framework's default deletion model is soft-delete: AuditableBaseEntity.Delete() sets IsDeleted = true and EF Core global query filters exclude the row from normal queries.…","i":"AuditableBaseEntity.Delete OutboxMessage IsDeleted true"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#decision","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Decision","x":"Separate the two concerns and provide an extension point for each, rather than overloading soft-delete: 1. Soft-delete stays the default for lifecycle/state management (hide +…","i":"MMCA.Common.Domain.Attributes.PiiAttribute MMCA.Common.Domain.Interfaces MMCA.Common.Domain.Privacy EncryptedStringConverter PiiConventionTestsBase OutboxCleanupService IAnonymizable PiiRedactor Anonymize Result User Pii"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#rationale","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Rationale","x":"- Right tool per concern: soft-delete answers \"is this record active?\"; erasure answers \"has this person's data been removed?\". Conflating them (e.g. hard-deleting inside…","i":"Delete"},{"u":"/docs/adr/005-soft-delete-vs-erasure.html#trade-offs","d":"ADR-005: Soft-Delete vs. Right-to-Erasure","k":"Architecture Decision Records","t":"Trade-offs","x":"- Erasure is opt-in per entity, but the opt-in is guarded: an aggregate exposing a [Pii]-marked property that does not implement IAnonymizable fails the architecture fitness…","i":"IAnonymizable Pii"},{"u":"/docs/adr/006-database-per-service.html","d":"ADR-006: Database per Service","k":"Architecture Decision Records"},{"u":"/docs/adr/006-database-per-service.html#status","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-07). Supersedes the earlier \"deliberately one shared database\" stance. Clarified 2026-06-27: the single context class became one sealed context class per engine…","i":"Name"},{"u":"/docs/adr/006-database-per-service.html#context","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Context","x":"When the modules were first extracted into independently-deployable services, all services in an app still pointed at a single shared SQL database with a single OutboxMessages…","i":"CrossDataSourceDegradeConvention EntityDataSourceRegistry DataSourceResolver DbContextFactory OutboxProcessor OutboxMessages"},{"u":"/docs/adr/006-database-per-service.html#decision","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Decision","x":"Adopt database-per-service: each service owns its own physical database with its own OutboxMessages table. - One sealed concrete context class per engine, one instance per…","i":"CrossDataSourceDegradeConvention PhysicalDbContextFactory ApplicationDbContext INavigationPopulator DataSourceResolver SQLServerDbContext ADC_Notification CosmosDbContext OutboxProcessor SqliteDbContext ADC_Conference ADC_Engagement"},{"u":"/docs/adr/006-database-per-service.html#rationale","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Rationale","x":"- Removes the shared-outbox race (the sharpest cost of the shared DB) without an OriginService filter: physical isolation is simpler and stronger than a logical filter. - Real…","i":"OriginService"},{"u":"/docs/adr/006-database-per-service.html#trade-offs","d":"ADR-006: Database per Service","k":"Architecture Decision Records","t":"Trade-offs","x":"- No cross-database FKs or transactions. Relationships that span services degrade to scalar IDs; consistency across services is eventual (outbox + broker), not transactional. -…"},{"u":"/docs/adr/007-grpc-extraction.html","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records"},{"u":"/docs/adr/007-grpc-extraction.html#status","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-23 (the [ServiceContract] marker now has a dedicated fitness rule behind it, ServiceContractPurityTestsBase, subclassed in all four repos; it is a…","i":"ServiceContractPurityTestsBase ServiceContract"},{"u":"/docs/adr/007-grpc-extraction.html#context","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Context","x":"Once modules became separate service processes, the in-process interface calls between them (e.g. Conference → Engagement's IBookmarkCountService, Engagement → Conference's…","i":"ISessionBookmarkValidationService IBookmarkCountService Result"},{"u":"/docs/adr/007-grpc-extraction.html#decision","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Decision","x":"Use gRPC, exposed through MMCA.Common.Grpc, with a contract-package convention: - .Contracts projects hold the .proto definitions plus a gRPC adapter that implements the same…","i":"SessionBookmarkValidationServiceGrpcAdapter GrpcResultExceptionInterceptor JwtForwardingClientInterceptor Directory.Build.props AddTypedGrpcClient SocketsHttpHandler MMCA.Common.Grpc HandleFailure IReadOnlyList RpcException serviceName Contracts"},{"u":"/docs/adr/007-grpc-extraction.html#rationale","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Rationale","x":"- No business-logic rewrite: the gRPC adapter implements the interface modules already depend on; swapping in-process for cross-process is a registration change. - Transport…","i":"MicroserviceExtractionTests ServiceContract MassTransit version proto"},{"u":"/docs/adr/007-grpc-extraction.html#trade-offs","d":"ADR-007: Synchronous Cross-Service Calls via gRPC Contracts","k":"Architecture Decision Records","t":"Trade-offs","x":"- Bidirectional pairs need care. Conference ↔ Engagement is a mutual gRPC pair; the AppHost deliberately omits a reciprocal WaitFor to avoid a startup deadlock: transient \"peer…","i":"Http1AndHttp2 WaitFor Http2 grpc"},{"u":"/docs/adr/008-service-extraction-topology.html","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records"},{"u":"/docs/adr/008-service-extraction-topology.html#status","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Status","x":"Accepted. Amended by ADR-089 (2026-08-18): the Gateway keeps the route-to-service map this record gave it, but stops expressing it as MapForwarder calls in code. YARP…","i":"MapForwarder ReverseProxy"},{"u":"/docs/adr/008-service-extraction-topology.html#context","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Context","x":"ADC began as a modular monolith: one MMCA.ADC.WebAPI host loaded every module (Identity, Conference, Engagement, Notification) in-process via the ModuleLoader, sharing one…","i":"MMCA.ADC.WebAPI ModuleLoader"},{"u":"/docs/adr/008-service-extraction-topology.html#decision","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Decision","x":"Extract one service host per module: MMCA.ADC.{Identity,Conference,Engagement,Notification}.Service and front them with a single YARP reverse-proxy Gateway (MMCA.ADC.Gateway,…","i":"MicroserviceExtractionTests MMCA.ADC.Gateway MMCA.ADC.WebAPI ModuleLoader Notification Conference Engagement Identity MMCA.ADC Modules Service Module"},{"u":"/docs/adr/008-service-extraction-topology.html#rationale","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Rationale","x":"- No business-logic rewrite. Because a service is just the monolith with one module enabled, extraction was a hosting/wiring change, not a domain change, and the module-isolation…","i":"ModuleLoader"},{"u":"/docs/adr/008-service-extraction-topology.html#trade-offs","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Trade-offs","x":"- Operational complexity. Four deployables plus a Gateway, service discovery, a broker, and per-service databases, versus one process. Mitigated locally by Aspire orchestration…","i":"MMCA.Common.API ServiceDefaults Http1AndHttp2 Http2"},{"u":"/docs/adr/008-service-extraction-topology.html#applicability","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Applicability","x":"This ADR is framed around ADC (the first repo extracted), but the same topology is now the framework's standard extraction shape, not an ADC-only choice. MMCA.Store followed it:…","i":"MMCA.Store.Gateway MMCA.Store.WebAPI MMCA.Store Identity Catalog Service Sales"},{"u":"/docs/adr/008-service-extraction-topology.html#related","d":"ADR-008: Extraction of the Modular Monolith into Per-Module Services + Gateway","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (outbox dual dispatch), ADR-004 (cross-service token validation via JWKS), ADR-006 (database per service), and ADR-007 (gRPC cross-service calls) are the facet decisions…"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#status","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-14). Amended by ADR-087 (2026-08-18): the resilience objective extends past outbound HTTP and gRPC clients for the first time, to the outbox's broker publish,…"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#context","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Context","x":"The framework already supplies the mechanisms for surviving partial failure: a standard Polly resilience handler (timeout / retry / circuit breaker), the outbox for at-least-once…","i":"ConfigureHttpClientDefaults AddTypedServiceClient AddTypedGrpcClient MMCA.Common.Aspire"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#decision","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Decision","x":"1. Resilience is a framework invariant, not a per-call choice. Every outbound HttpClient and gRPC client registered through the framework's extension methods (AddTypedGrpcClient,…","i":"MMCA.Common.Grpc.Tests ResilienceHandlerTests AddTypedServiceClient AddTypedGrpcClient MMCA.Common.Aspire HttpClient"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#rationale","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Rationale","x":"- Invariant over discipline. A fitness function turns \"remember to add resilience\" into a build gate: the same approach the framework already uses for the layer rules and the…"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#trade-offs","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Trade-offs","x":"- The named gate (ResilienceHandlerTests, MMCA.Common.Grpc.Tests) asserts that the gRPC client path (AddTypedGrpcClient) registers the standard handler, not the runtime behavior…","i":"ResilienceCircuitBreakerFaultInjectionTests MMCA.Common.Grpc.Tests ResilienceHandlerTests AddTypedServiceClient AddTypedGrpcClient"},{"u":"/docs/adr/009-resilience-and-recovery-objectives.html#revision-2026-08-18","d":"ADR-009: Resilience Policies & Recovery Objectives","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"This record's first Decision point scoped resilience to \"every outbound HttpClient and gRPC client registered through the framework's extension methods\". That scope was accurate…","i":"BrokerResilienceDefaults BrokenCircuitException HttpResilienceDefaults CommandTimeoutSeconds EnableRetryOnFailure ResiliencePipeline DbContextFactory OutboxProcessor HttpClient"},{"u":"/docs/adr/010-integration-event-schema-versioning.html","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#status","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-19). Updated 2026-06-27 (Helpdesk enforcement gap closed; all three consumers now gate the convention). Updated 2026-08-14 (ADC now gates seven events, and a…","i":"OutputCacheEvictionRequested"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#context","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Context","x":"Integration events cross service boundaries (Identity → Conference, Conference ↔ Engagement, …) and are resolved by consumers solely by their type string: the outbox serializes…","i":"OutboxMessage.FromDomainEvent DateOccurred EventType MessageId"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#decision","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Decision","x":"1. Every integration event carries an explicit SchemaVersion. BaseIntegrationEvent exposes public virtual int SchemaVersion = 1;. It is serialized with the payload…","i":"MMCA.Common.Testing.Architecture EventConventionTestsBase BaseIntegrationEvent IIntegrationEvent UserRegisteredV2 SchemaVersion virtual public int"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#rationale","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Rationale","x":"- A signal, enforced. A version field plus a build-gating convention test turns \"remember the contract\" into something the tooling checks: the same invariant-over-discipline…","i":"virtual"},{"u":"/docs/adr/010-integration-event-schema-versioning.html#trade-offs","d":"ADR-010: Integration-Event Schema Versioning & Upcaster Policy","k":"Architecture Decision Records","t":"Trade-offs","x":"- SchemaVersion is a signal, not a mechanism: by itself it does not stop a consumer breaking on a real reshape. The load-bearing half is the discipline (new type + upcaster). At…","i":"RegisterUpcastedIntegrationEventConsumer MMCA.Helpdesk.Architecture.Tests EventVersioningConventionTests ProductCreatedIntegrationEvent MMCA.Store.Architecture.Tests OutputCacheEvictionRequested TicketOpenedIntegrationEvent MMCA.ADC.Architecture.Tests OrderPlacedIntegrationEvent EventConventionTestsBase CommonArchitectureMap map.ModuleNames.Count"},{"u":"/docs/adr/011-single-locale-i18n.html","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records"},{"u":"/docs/adr/011-single-locale-i18n.html#status","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Status","x":"Superseded by ADR-027 (2026-06-27). Originally Accepted (2026-06-19). The \"if multi-locale is ever required\" scope below is the blueprint ADR-027 implements; this record is…"},{"u":"/docs/adr/011-single-locale-i18n.html#context","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Context","x":"The MMCA applications (the ADC conference app, the Store) and the MMCA.Common.UI library currently ship a single locale (en-US). The architecture rubric scores…","i":"MMCA.Common.UI"},{"u":"/docs/adr/011-single-locale-i18n.html#decision","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Decision","x":"1. Single-locale (en-US) is an explicit non-goal for now. User-facing strings are inline in markup; dates/numbers use invariant or fixed formatting where appropriate. 2. The…","i":"RequestLocalization"},{"u":"/docs/adr/011-single-locale-i18n.html#rationale","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Rationale","x":"- Recording the decision converts an implicit rubric-zero into a conscious, revisitable choice: the same posture as the single-region DR acceptance in ADR-009. - Premature i18n…"},{"u":"/docs/adr/011-single-locale-i18n.html#trade-offs","d":"ADR-011: Single-Locale by Design (No Internationalization)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Adding a locale later touches every view plus the formatting paths: a real but bounded effort, accepted. - Hard-coded strings make a future extraction larger; mitigated by the…"},{"u":"/docs/adr/012-grpc-host-transport.html","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records"},{"u":"/docs/adr/012-grpc-host-transport.html#status","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Status","x":"Accepted (re-verified against source 2026-08-14)."},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-06-22-store-converged-to-profile-a","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-06-22): Store converged to Profile A","x":"Store originally chose Profile B, but its cross-service gRPC failed in Azure Container Apps. With Http1AndHttp2 Kestrel + transport: 'auto' ingress on a cleartext endpoint there…","i":"IProductVariantService.ExistsAsync IUserSalesExportService HTTP_1_1_REQUIRED WithJwksDiscovery AddItemCommand Http1AndHttp2 transport identity gateway httpGet Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-07-09-adc-notification-adds-a-mixed-endpoint-profile-per-endpoint-protocols","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-07-09): ADC Notification adds a mixed-endpoint profile (per-endpoint protocols)","x":"The live-channel push pipeline (ADR-039) gave ADC's Notification service an inbound cleartext gRPC server (LiveChannelPushService.PushToChannel, called best-effort by Engagement…","i":"LiveChannelPushService.PushToChannel engagementService.WithReference services__notification__grpc__0 appsettings.Development.json additionalPortMappings notificationService Http1AndHttp2 httpGet WaitFor Http2 grpc http"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-07-25-probe-listeners-are-adcs-answer-not-tcp-probes-and-gateway-routed-jwks-is-a-local-only-rule","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-07-25): probe listeners are ADC's answer, not TCP probes; and gateway-routed JWKS is a local-only rule","x":"Two claims above were written from an earlier state of the code and no longer describe either app. 1. ADC probes never touch the traffic endpoint; TCP probes were then…","i":"HTTP_1_1_REQUIRED WithJwksDiscovery identityApp.name Program.cs tcpSocket transport identity gateway httpGet Http1 grpc"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-07-28-the-probe-listener-is-the-single-pattern-in-both-apps-no-tcp-probes-anywhere","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-07-28): the probe listener is the single pattern in both apps (no TCP probes anywhere)","x":"Store PR 55 (commit 297064bb, merged 2026-07-27) ported ADC's dedicated probe listener to Store, so the Store-only tcpSocket exception recorded in the 2026-07-25 update above is…","i":"HealthProbe__Port Http1AndHttp2 tcpSocket httpGet Http1 Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-08-07-the-probe-listener-moved-into-mmcacommon-and-notifications-grpc-endpoint-carries-a-second-service","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-08-07): the probe listener moved into MMCA.Common, and Notification's gRPC endpoint carries a second service","x":"1. One shared framework method, not a per-service file. The KestrelConfiguration.cs copies the two updates above cite no longer exist in either app. The pattern was extracted…","i":"UserNotificationExportGrpcService MMCA.ADC.Notification.Contracts services__notification__grpc__0 identityService.WithReference appsettings.Development.json HttpProtocols.Http1AndHttp2 redeclareCleartextEndpoint ConfigureEndpointDefaults KestrelConfiguration.cs additionalPortMappings ASPNETCORE_ENVIRONMENT LiveChannelGrpcService"},{"u":"/docs/adr/012-grpc-host-transport.html#update-2026-08-14-stores-sales-runs-the-mixed-endpoint-profile-too-so-no-pure-profile-b-host-remains","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Update (2026-08-14): Store's Sales runs the mixed-endpoint profile too, so no pure Profile B host remains","x":"Sales gained an inbound gRPC edge of its own (IUserSalesExportService, the Identity-driven data-subject export), and it resolved that the same way ADC's Notification did: not by…","i":"identityService.WithReference appsettings.Development.json UserSalesExportGrpcService AddSalesUserExportClient services__sales__grpc__0 IUserSalesExportService additionalPortMappings RequireAuthorization HealthProbe__Port Http1AndHttp2 salesService _grpc.sales"},{"u":"/docs/adr/012-grpc-host-transport.html#context","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Context","x":"Once modules were extracted into separate service hosts (ADR-008) that call each other synchronously over gRPC (ADR-007), each service's Kestrel had to serve both REST traffic…","i":"HTTP_1_1_REQUIRED Http1AndHttp2 HttpClient Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#decision","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Decision","x":"Pick one of two coherent transport profiles per app, and wire the gateway forwarder and JWKS discovery to match. Use when services must serve gRPC on cleartext (any bidirectional…","i":"builder.ConfigureEndpointsWithHealthProbe UserNotificationExportGrpcService HttpProtocols.Http1AndHttp2 ConfigureEndpointDefaults LiveChannelGrpcService HttpVersion.Version20 RequestVersionOrLower HttpProtocols.Http2 RequestVersionExact HTTP_1_1_REQUIRED WithJwksDiscovery Http1AndHttp2"},{"u":"/docs/adr/012-grpc-host-transport.html#rationale","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Rationale","x":"- The Kestrel protocol choice is the root constraint; the gateway-forward mode and the JWKS authority are downstream consequences, not independent knobs. Documenting them as a…","i":"HTTP_1_1_REQUIRED Http2"},{"u":"/docs/adr/012-grpc-host-transport.html#trade-offs","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two profiles to keep straight. A service that gains an inbound gRPC edge must migrate from Profile B to Profile A and flip ForwardHttp2 and the JWKS wiring together, or it…","i":"appsettings.Development.json additionalPortMappings appsettings.json Http1AndHttp2 ForwardHttp2 transport http2"},{"u":"/docs/adr/012-grpc-host-transport.html#related","d":"ADR-012: gRPC-Host Transport Convention (Http2-only h2c vs. Http1AndHttp2 + ALPN)","k":"Architecture Decision Records","t":"Related","x":"- ADR-004 (cross-service token validation via JWKS / OIDC discovery), ADR-007 (gRPC cross-service calls), ADR-008 (monolith → services + gateway topology), ADR-039 (live-channel…"},{"u":"/docs/adr/013-result-pattern.html","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records"},{"u":"/docs/adr/013-result-pattern.html#status","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-07-21 (exception-handler chain / ProblemDetails edge contract documented)."},{"u":"/docs/adr/013-result-pattern.html#context","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Context","x":"Operations at every layer fail in expected ways: input is invalid, a domain invariant is broken, a requested entity is missing, a uniqueness conflict occurs, the caller lacks…"},{"u":"/docs/adr/013-result-pattern.html#decision","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Decision","x":"Model expected failures as values using Result / Result (MMCA.Common.Shared.Abstractions), not exceptions. - A Result is either success or failure; a failure carries one or more…","i":"OperationCanceledExceptionHandler ApiControllerBase.HandleFailure MMCA.Common.Shared.Abstractions GrpcResultExceptionInterceptor AddCommonExceptionHandlers OperationCanceledException ValidationExceptionHandler DbUpdateExceptionHandler DomainExceptionHandler GlobalExceptionHandler UnprocessableEntity ValidationException"},{"u":"/docs/adr/013-result-pattern.html#rationale","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Rationale","x":"- Failures are in the signature. A method that can fail returns Result , so the caller cannot silently ignore the failure path the way an uncaught exception allows. - Category,…","i":"Result.Failure HandleFailure ErrorType IsFailure requestId Result"},{"u":"/docs/adr/013-result-pattern.html#trade-offs","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Trade-offs","x":"- More ceremony at call sites than letting an exception bubble; the combinators absorb most of it. - Two error channels coexist (Result for expected, exceptions for exceptional).…","i":"GlobalExceptionHandler ErrorType Result"},{"u":"/docs/adr/013-result-pattern.html#related","d":"ADR-013: Result Pattern over Exceptions for Flow Control","k":"Architecture Decision Records","t":"Related","x":"ADR-007 (Result over the wire via gRPC), ADR-014 (the decorator pipeline returns Result.Failure to short-circuit a command before it reaches the handler).","i":"Result.Failure"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#status","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-07-19 (Transactional semantics: rollback on business failure + post-commit event dispatch; see Revision below). Revised 2026-08-18 (the pipeline order…"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#context","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Context","x":"Commands and queries share cross-cutting concerns: validation, transactions, cache invalidation, logging / timing, and feature gating. Putting that logic inside each handler…"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#decision","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Decision","x":"Use single-responsibility handlers behind a Scrutor-composed decorator pipeline. - ICommandHandler and IQueryHandler (MMCA.Common.Application) are one handler per use case, each…","i":"ModuleLoader.DiscoverAndRegister ScanModuleApplicationServices ProfilingCommandDecorator AddApplicationDecorators AddApplicationProfiling MMCA.Common.Application ProfilingQueryDecorator ICacheInvalidating AddInfrastructure ICommandHandler IQueryCacheable AddApplication"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#rationale","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Rationale","x":"- Thin, testable handlers. A handler has no transaction, logging, or caching plumbing, so it is unit-tested in isolation. - One place to read and change the pipeline. The order…"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#trade-offs","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Trade-offs","x":"- Registration order is the reverse of execution order (a Scrutor foot-gun), mitigated by the inline ordering comments in AddApplicationDecorators(). - Decorators must be…","i":"AddApplicationDecorators"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#revision-2026-07-19","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Revision (2026-07-19)","x":"Two Transactional-decorator semantics changed with the 2026-07-19 full review: - A returned business failure now rolls the transaction back. Previously a handler returning…","i":"DbContextFactory.ExecuteInTransactionAsync DomainEventSaveChangesInterceptor RollbackTransaction Result.Failure IsFailure Result"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#revision-2026-08-18","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Two decorators were added to both chains, so the order recorded in the Decision above is no longer the shipped one. The registration site is unchanged in kind:…","i":"CancellationTokenSource.CreateLinkedTokenSource cqrs.authorization.denied.count DecoratorPipelineOrderTestsBase AuthorizationCommandDecorator ExpectedCommandDecorators AddApplicationDecorators ExpectedQueryDecorators AuthorizationDenied ICurrentUserService IPermissionRegistry IRequiresPermission budget.CancelAfter"},{"u":"/docs/adr/014-cqrs-decorator-pipeline.html#related","d":"ADR-014: CQRS Handlers with a Decorator Pipeline","k":"Architecture Decision Records","t":"Related","x":"ADR-013 (Result, the short-circuit currency of the pipeline, and the Failure error type the timeout decorator reuses because the taxonomy has no timeout member), ADR-003…","i":"IPermissionRegistry MMCA.Common.Cqrs HasPermission SaveChanges Failure"},{"u":"/docs/adr/015-architecture-fitness-functions.html","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records"},{"u":"/docs/adr/015-architecture-fitness-functions.html#status","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-18 (two new rule families, namespace dependency cycles and trailing CancellationToken declarations, plus a third enforcement layer: a compile-time…","i":"CancellationToken proto"},{"u":"/docs/adr/015-architecture-fitness-functions.html#context","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Context","x":"The codebase rests on invariants that are easy to state and easy to erode by accident: clean- architecture layer flow (Domain depends on nothing above it), module isolation (no…","i":"SchemaVersion"},{"u":"/docs/adr/015-architecture-fitness-functions.html#decision","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Decision","x":"Enforce architectural invariants as automated checks that gate the build, in two layers (a third joined them on 2026-08-18: see the Revision at the end). 1. Compile-time guard.…","i":"MMCA.Common.LayerEnforcement.targets MMCA.Common.Testing.Architecture HelpdeskArchitectureMap CommonArchitectureMap StoreArchitectureMap AdcArchitectureMap IArchitectureMap ProjectReference dotnet test"},{"u":"/docs/adr/015-architecture-fitness-functions.html#rationale","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Rationale","x":"- Invariant over discipline. Turning \"do not do X\" into a red build is the only enforcement that scales. It is the same lever used by the layer rules, the resilience gate…","i":"IArchitectureMap"},{"u":"/docs/adr/015-architecture-fitness-functions.html#trade-offs","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Trade-offs","x":"- The tests assert structure / registration, not runtime behavior. ADR-009's test proves a client wires resilience, not that its policy values are correct; parameter tuning stays…","i":"FrameworkSanityTests IArchitectureMap"},{"u":"/docs/adr/015-architecture-fitness-functions.html#revision-2026-08-18","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Two new rule families joined the shared library, and a third enforcement layer joined the two the Decision above describes. The counts in MMCA.Common/FACTS.md move with them: 102…","i":"AsyncMethodsDeclareTrailingCancellationToken Microsoft.CodeAnalysis.PublicApiAnalyzers ArchitectureRules.CancellationTokens dotnet_analyzer_diagnostic.severity NamespacesHaveNoDependencyCycles MMCA.Common.Infrastructure Context.ConnectionAborted IHostedService.StartAsync ArchitectureRules.Cycles TenancySettingsValidator InternalAPI.Shipped.txt NamespaceCycleTestsBase"},{"u":"/docs/adr/015-architecture-fitness-functions.html#revision-2026-08-18-section-b-rule-families","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Revision (2026-08-18): Section B rule families","x":"A second entry on the same date, kept separate rather than folded into the one above because it lands with a different wave and changes a number that revision states. Two rule…","i":"IdempotencyConventionTestsBase ArchitectureRules.Protos ProtoContractTestsBase PublicAPI.Shipped.txt FrozenProtoContracts csharp_namespace SolutionFileName FactsGenerator justification NonIdempotent Idempotent ProtoFiles"},{"u":"/docs/adr/015-architecture-fitness-functions.html#revision-2026-08-23-superseded-counts-a-re-anchored-citation-and-the-real-test-floor","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Revision (2026-08-23): superseded counts, a re-anchored citation, and the real test floor","x":"No rule family joined or left the library in this entry. It corrects three things the two 2026-08-18 revisions above state, and it is kept as its own entry rather than edited…","i":"Microsoft.CodeAnalysis.PublicApiAnalyzers SpecificationFitnessTests MMCA.Common.UI.E2E.Tests FrameworkSanityTests FactsGenerator FACTS.md"},{"u":"/docs/adr/015-architecture-fitness-functions.html#related","d":"ADR-015: Architecture Invariants Enforced as Fitness Functions","k":"Architecture Decision Records","t":"Related","x":"ADR-009 (resilience gate), ADR-010 (event-version gate), ADR-016 (MassTransit pin gate, and the lockstep release cadence the public API baseline is pinned to), ADR-006/007/008…","i":"CancellationToken"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#status","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Amended (2026-07-28): the fitness function now gates two commercial-license majors (MassTransit and SixLabors.ImageSharp), so the decision is restated as…","i":"MassTransit.Azure.ServiceBus.Core Directory.Packages.props SixLabors.ImageSharp"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#context","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common publishes its MMCA.Common. NuGet package set (see FACTS.md for the authoritative list and count) consumed by three downstream repos: the two production apps (Store,…","i":"Directory.Packages.props Infrastructure MassTransit MT_LICENSE FACTS.md Domain"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#decision","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Decision","x":"1. Version the whole MMCA.Common. package set in lockstep. All packages share one version (MinVer, derived from a single vX.Y.Z git tag); a release tags every package (see…","i":"MassTransit.Azure.ServiceBus.Core RestorePackagesWithLockFile DependencyVersionTestsBase MMCA.Common.Infrastructure Directory.Packages.props MassTransit.RabbitMQ SixLabors.ImageSharp MassTransit MT_LICENSE FACTS.md Obsolete vX.Y.Z"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#rationale","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Rationale","x":"- One version, one compatibility story. Lockstep removes the N-package matrix: \"everything on vX.Y.Z\" is the only supported combination, which is the right trade for a small…","i":"vX.Y.Z"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#trade-offs","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Trade-offs","x":"- A consumer cannot adopt a single package in isolation: it takes the whole set at the new version. - Lockstep will bump a package whose code did not change (acceptable: the…","i":"MassTransit.Azure.ServiceBus.Core Directory.Packages.props dependabot.yml"},{"u":"/docs/adr/016-lockstep-versioning-masstransit-pin.html#related","d":"ADR-016: Lockstep Package Versioning and the MassTransit-v8 Pin","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (the fitness function that enforces the pins), ADR-003 / ADR-006 (MassTransit is the broker transport behind the outbox and database-per-service flows)."},{"u":"/docs/adr/017-request-idempotency.html","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records"},{"u":"/docs/adr/017-request-idempotency.html#status","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-01: the guard around execute-and-store is now an IDistributedLock resolved from DI (Redis-backed wherever a connection multiplexer is registered, which…","i":"IdempotencyConventionTestsBase IDistributedLock justification NonIdempotent ObjectResult Idempotent NoContent HttpPost"},{"u":"/docs/adr/017-request-idempotency.html#context","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Context","x":"Write endpoints (POST / PUT / PATCH) are exposed to client retries and double-submits: a flaky network, an impatient user double-clicking, or a resilience pipeline re-issuing a…","i":"Result"},{"u":"/docs/adr/017-request-idempotency.html#decision","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Decision","x":"Provide opt-in, client-driven request idempotency as an MVC action filter in MMCA.Common.API. - Opt-in per action. [Idempotent] (IdempotentAttribute, a ServiceFilterAttribute…","i":"IdempotencySettings.CacheExpirationHours InProcessDistributedLock IConnectionMultiplexer ServiceFilterAttribute KeyedSemaphoreStripe RedisDistributedLock IdempotentAttribute AddInfrastructure IdempotencyFilter IDistributedLock StatusCodeResult MMCA.Common.API"},{"u":"/docs/adr/017-request-idempotency.html#rationale","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Rationale","x":"- Safety at the edge, not in every handler. Deduplication lives in one filter, so a handler stays a thin use case (ADR-014) and does not grow ad-hoc \"did I already do this?\"…","i":"Idempotent"},{"u":"/docs/adr/017-request-idempotency.html#trade-offs","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Trade-offs","x":"- Cross-instance mutual exclusion follows Redis, so it is a deployment property, not a guarantee. Every ADC and Store service host registers a Redis IConnectionMultiplexer when a…","i":"IConnectionMultiplexer StatusCodeResult IAnonymizable ObjectResult Idempotent Location redis"},{"u":"/docs/adr/017-request-idempotency.html#revision-2026-08-18","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"The last Trade-off above is the one this revision addresses, and it does so by changing what is required. Nothing here makes an endpoint idempotent. What it requires is that…","i":"PostActions_ShouldDeclare_IdempotencyIntent IdempotencyConventionTestsBase AttributeTargets.Method NonIdempotentAttribute GetCustomAttributes AuthControllerBase EnableRateLimiting MMCA.Common.API AttributeUsage justification Justification NonIdempotent"},{"u":"/docs/adr/017-request-idempotency.html#related","d":"ADR-017: HTTP Request Idempotency via Client-Supplied Keys","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (handler idempotency for outbox/event consumers, a distinct concern), ADR-013 (Result is the response the filter caches/replays), ADR-014 (the filter keeps the handler…","i":"ICacheService NonIdempotent"},{"u":"/docs/adr/018-polyglot-persistence.html","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records"},{"u":"/docs/adr/018-polyglot-persistence.html#status","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Status","x":"Accepted. The framework plumbing is complete, covered by unit and integration tests (DataSourceResolverTests, CrossDataSourceDegradeConventionTests, EntityTypeConfigurationTests,…","i":"CrossDataSourceDegradeConventionTests CosmosConfigurationPortabilityTests MultiSourceSqliteIntegrationTests EntityTypeConfigurationTests DataSourceResolverTests FACTS.md Session Room"},{"u":"/docs/adr/018-polyglot-persistence.html#context","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Context","x":"ADR-006 (database-per-service) splits storage along the Name axis: several physically separate databases, all on the same engine (SQL Server), one per service. A second,…","i":"DataSourceKey Engine Name"},{"u":"/docs/adr/018-polyglot-persistence.html#decision","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Decision","x":"Support three storage engines behind one entity model and one set of repository abstractions, selected per entity configuration. 1. DataSource engine enum: SQLServer (full…","i":"CrossDataSourceDegradeConvention EntityTypeConfigurationSQLServer EntityTypeConfigurationCosmos EntityTypeConfigurationSqlite SQLServerMigrationsAssembly CosmosIntIdValueGenerator SQLServerConnectionString EntityDataSourceRegistry EntityTypeConfiguration CosmosConnectionString SqliteConnectionString ApplicationDbContext"},{"u":"/docs/adr/018-polyglot-persistence.html#rationale","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Rationale","x":"- Right store per access pattern, as a configuration decision. The engine becomes an attribute on a configuration class, not a rewrite. The same domain entity, application…"},{"u":"/docs/adr/018-polyglot-persistence.html#trade-offs","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Trade-offs","x":"- No cross-engine JOINs, FKs, or transactions. This is the ADR-006 cost made sharper: across engines it is a hard limit, not a deployment choice. A query spanning engines (for…","i":"SpecificationsDoNotNavigateToOtherEntities CrossSourceSpecification specifications keys"},{"u":"/docs/adr/018-polyglot-persistence.html#related","d":"ADR-018: Polyglot Persistence (Multiple Storage Engines Behind One Model)","k":"Architecture Decision Records","t":"Related","x":"ADR-006 (database-per-service: the Name axis this ADR's Engine axis is orthogonal to; they share DataSourceKey), ADR-002 (navigation populators bridge the relationships the…","i":"DataSourceKey"},{"u":"/docs/adr/019-rate-limiting.html","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records"},{"u":"/docs/adr/019-rate-limiting.html#status","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Status","x":"Accepted. Revised 2026-08-01 (the auth-ip per-IP anonymous-authentication limiter, which the shared auth controller applies to login and register by default, is recorded as the…","i":"RateLimitingSettings UserPolicy"},{"u":"/docs/adr/019-rate-limiting.html#context","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Context","x":"Every service exposes read and write endpoints to the public internet through the gateway (ADR-008). Abusive or runaway clients (scrapers, credential stuffing, retry storms, a…"},{"u":"/docs/adr/019-rate-limiting.html#decision","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Decision","x":"Rate limiting is layered, and the always-on global limiter is authenticated-only. 1. A global limiter that only caps authenticated callers. AddCommonRateLimiting…","i":"HttpContext.Connection.RemoteIpAddress EnableRateLimitingAttribute UseCommonMiddlewarePipeline AttributeUsage.Inherited LoginProtectionService AddCommonRateLimiting RateLimitPolicyAuthIp GetCustomAttributes UseForwardedHeaders AuthControllerBase EnableRateLimiting EndpointDataSource"},{"u":"/docs/adr/019-rate-limiting.html#rationale","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Rationale","x":"- Limit the traffic that is both attributable and expensive. An authenticated request is tied to a principal and usually drives the database; capping per-principal stops a single…"},{"u":"/docs/adr/019-rate-limiting.html#trade-offs","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Trade-offs","x":"- The global limiter only protects the authenticated surface. The anonymous surface is covered endpoint by endpoint instead: login and register carry the auth-ip limiter by…","i":"ForwardLimit"},{"u":"/docs/adr/019-rate-limiting.html#revision-2026-08-18","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"The layering above is unchanged: the global limiter is still authenticated-only, infrastructure and anonymous traffic are still exempt, auth-ip still covers login and register by…","i":"RateLimitAlgorithm.FixedWindow RedisFixedWindowRateLimiter IConnectionMultiplexer Interlocked.Exchange RateLimitingSettings StringIncrementAsync PerUserPermitLimit AuthIpPermitLimit GlobalPermitLimit SegmentsPerWindow allowDistributed SlidingWindow"},{"u":"/docs/adr/019-rate-limiting.html#related","d":"ADR-019: Layered Rate Limiting with an Authenticated-Only Global Limiter","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the JWKS/discovery traffic the limiter exempts, and the authenticated principal it keys on), ADR-008 (the gateway edge this protects), ADR-017 (request idempotency, the…","i":"RateLimitingSettings IncrementAsync UseRateLimiter Distributed INCR"},{"u":"/docs/adr/020-permission-based-authorization.html","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records"},{"u":"/docs/adr/020-permission-based-authorization.html#status","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-25, amended 2026-07-10 and 2026-08-23)."},{"u":"/docs/adr/020-permission-based-authorization.html#context","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Context","x":"Authorization started as pure role-based access control (RBAC). Endpoints declared the role they required with [Authorize(Policy = ...)] against named policies: RequireOrganizer,…","i":"RequireAuthenticatedUser RequireAuthenticated RequireOrganizer RequireAttendee RequireAdmin RequireRole Authorize Policy"},{"u":"/docs/adr/020-permission-based-authorization.html#decision","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Decision","x":"Add a permission (capability) layer over RBAC, opt-in and backward-compatible. - A central registry maps roles to permissions. IPermissionRegistry / PermissionRegistry…","i":"DefaultAuthorizationPolicyProvider PermissionAuthorizationHandler AuthClaimTypes.Permission PermissionRegistryBuilder AddAuthorizationPolicies PermissionPolicyProvider RequireAuthenticatedUser MMCA.Common.Shared.Auth RoleNames.ContentEditor HasPermissionAttribute PermissionRequirement IPermissionRegistry"},{"u":"/docs/adr/020-permission-based-authorization.html#rationale","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Rationale","x":"- Capabilities decouple endpoints from roles. A route says what it does (conference:sessions:manage), and who may do it is a registry decision, so adding ContentEditor with a…","i":"ContentEditor"},{"u":"/docs/adr/020-permission-based-authorization.html#trade-offs","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Trade-offs","x":"- It is still RBAC, not ABAC. The model resolves role to permission; it does not evaluate resource or attribute conditions. Per-resource ownership (\"a customer may read only…","i":"ConferencePermissions OwnerOrAdminFilter AddPermissions IAnonymizable Idempotent Grant"},{"u":"/docs/adr/020-permission-based-authorization.html#related","d":"ADR-020: Permission-Based Authorization Layered over Roles","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the authenticated principal and claims this keys on, including the optional permission claim), ADR-008 (each extracted service authorizes independently, so the registry…","i":"permission"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#status","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-09; adoption reviewed 2026-07-15). Revised 2026-08-18 (the inbox stays opt-in, but being off is no longer silent: a broker-connected host running NoOpInboxStore…","i":"NoOpInboxStore InboxMessages"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#context","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Context","x":"ADR-003 makes integration-event delivery at-least-once: the outbox guarantees a published event is not lost, and the MassTransit broker redelivers on consumer failure.…"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#decision","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Decision","x":"Add an opt-in inbox that records each successfully-processed integration event by its MessageId and skips redeliveries. - Every event carries a MessageId. BaseDomainEvent stamps…","i":"IX_InboxMessages_MessageId IntegrationEventConsumer SessionFeedbackSubmitted SpeakerUnlinkedFromUser EventFeedbackSubmitted AlreadyProcessedAsync ProductVariantChanged OutboxCleanupService SpeakerLinkedToUser AddBrokerMessaging MarkProcessedAsync AttendeeCheckedIn"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#rationale","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Rationale","x":"- Dedup once, not in every handler. A single consume-edge check turns \"every handler author must remember to be idempotent against redelivery\" into a framework guarantee for the…","i":"NoOpInboxStore"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#trade-offs","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Trade-offs","x":"- Not exactly-once. The crash-after-handler-before-inbox window reprocesses once, so handlers must stay idempotent for it; the inbox narrows the duplicate window, it does not…","i":"InboxMessages EnableInbox MessageId"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#related","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox and at-least-once delivery whose consumer side this deduplicates; handler idempotency is still required for the crash window), ADR-006 (the inbox lives in the…","i":"OutboxCleanupService InProcess"},{"u":"/docs/adr/021-consumer-inbox-idempotency.html#revision-2026-08-18","d":"ADR-021: Consumer-Side Inbox for Integration-Event Idempotency","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"The decision is unchanged: the inbox is still opt-in and NoOpInboxStore is still the default. What changed is that the default is now loud. 1. A broker-connected host with no…","i":"ApplicationDbContext.OnModelCreating MessageBusProvider.InProcess InboxDisabledWarningService IX_InboxMessages_MessageId IEntityTypeConfiguration base.OnModelCreating AddBrokerMessaging SQLServerDbContext AddInboxMessages CosmosDbContext SqliteDbContext ConfigureInbox"},{"u":"/docs/adr/022-browser-session-cookie-auth.html","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#status","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Status","x":"Accepted."},{"u":"/docs/adr/022-browser-session-cookie-auth.html#context","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Context","x":"The apps are Blazor Web Apps: a server-rendered (SSR) prerender pass runs on the first request, then an interactive phase (Blazor Server or WebAssembly) takes over.…","i":"Authorization localStorage Authorize"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#decision","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Decision","x":"Carry the session in HttpOnly cookies and add an authentication scheme that reads them during SSR prerender. The mechanism ships in MMCA.Common.API (SessionCookies/) with a…","i":"SessionCookieAuthenticationHandler CookieSessionRefresher mmca_auth_refresh HttpContext.User mmca_auth_access SessionCookieJar MMCA.Common.API MMCA.Common.UI Authorize DELETE POST"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#rationale","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Rationale","x":"- Fixes the fresh-GET prerender gap. Without a server-readable session, every deep-link or F5 to an [Authorize] page would redirect to /login despite a valid session; the cookie…","i":"Authorize"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#trade-offs","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Trade-offs","x":"- A non-validating auth scheme exists. SessionCookieAuthenticationHandler trusts a cookie it does not cryptographically verify. This is sound only because (a) the cookie is…","i":"SessionCookieAuthenticationHandler ISessionCookieSync"},{"u":"/docs/adr/022-browser-session-cookie-auth.html#related","d":"ADR-022: Browser Session-Cookie Authentication for Blazor SSR","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the JWT/JWKS validation the API performs on every call, which is why the SSR handler can skip signature validation), ADR-008 (the gateway and topology the UI talks to),…"},{"u":"/docs/adr/023-security-response-headers.html","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records"},{"u":"/docs/adr/023-security-response-headers.html#status","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02)."},{"u":"/docs/adr/023-security-response-headers.html#context","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Context","x":"Every client-facing host (the YARP Gateway and the Blazor UI web host in each app) must stamp the same hardened HTTP response headers: X-Content-Type-Options, X-Frame-Options,…"},{"u":"/docs/adr/023-security-response-headers.html#decision","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Decision","x":"Ship one security-headers middleware in MMCA.Common.Aspire (MMCA.Common.Aspire.Security), registered with AddCommonSecurityHeaders(configuration?, configure?) and inserted early…","i":"SecurityHeadersSettings.ContentSecurityPolicy SecurityHeadersMiddlewareTests MMCA.Common.Aspire.Security SecurityHeadersMiddleware AddCommonSecurityHeaders MMCA.Common.Aspire.Tests UseCommonSecurityHeaders BlazorCspPolicyProvider SecurityHeadersSettings StaticCspPolicyProvider AddCommonBlazorCsp ICspPolicyProvider"},{"u":"/docs/adr/023-security-response-headers.html#rationale","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Rationale","x":"- One hardened default, defined once. Centralizing the header set removes per-host drift and makes a new edge host secure by default rather than by remembering to copy headers. -…","i":"ICspPolicyProvider"},{"u":"/docs/adr/023-security-response-headers.html#trade-offs","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Trade-offs","x":"- The baseline CSP is intentionally incomplete. An API/Gateway host gets default-src 'self'-style protection but no script-src/style-src discipline unless it registers a fuller…","i":"SecurityHeadersSettings.ContentSecurityPolicy AddCommonSecurityHeaders BlazorCspPolicyProvider AddCommonBlazorCsp ICspPolicyProvider MMCA.Common.UI.Web TryAddSingleton ApiSettings"},{"u":"/docs/adr/023-security-response-headers.html#related","d":"ADR-023: Centralized Security-Response-Headers Middleware with a Pluggable CSP","k":"Architecture Decision Records","t":"Related","x":"ADR-019 (rate limiting, the other always-on edge protection living in the same Aspire layer), ADR-022 (browser session-cookie auth, the other browser-edge security control),…"},{"u":"/docs/adr/024-push-notifications.html","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records"},{"u":"/docs/adr/024-push-notifications.html#status","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27, amended 2026-07-15). Revised 2026-08-07 (transactional email recorded as an app-level concern outside the channel model; see Revision below). Revised…","i":"Enabled"},{"u":"/docs/adr/024-push-notifications.html#context","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Context","x":"The framework needs to deliver user-facing notifications (an organizer broadcasting a schedule change, a per-user alert). Two delivery models each fail on their own. A pure…"},{"u":"/docs/adr/024-push-notifications.html#decision","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Decision","x":"Deliver notifications over two channels from one application use case, with the transport and the recipient policy both behind abstractions. - A durable per-user inbox plus a…","i":"NullNotificationRecipientProvider PushNotificationSettings.Enabled INotificationRecipientProvider SignalRPushNotificationSender SendPushNotificationHandler SignalRLiveChannelPublisher MMCA.Common.Infrastructure NullPushNotificationSender IPushNotificationSender MMCA.Common.Application CancellationToken.None NotificationHubService"},{"u":"/docs/adr/024-push-notifications.html#rationale","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Rationale","x":"- Each channel covers the other's failure mode. The inbox guarantees eventual delivery to offline users; the push gives connected users immediacy. Persisting the inbox before…","i":"INotificationRecipientProvider IPushNotificationSender IMessageBus"},{"u":"/docs/adr/024-push-notifications.html#trade-offs","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Fan-out write amplification. One UserNotification row is written per recipient, so a broadcast to a large audience is a large insert. This is fine for the current per-event /…","i":"NullPushNotificationSender AddPushNotifications PushNotification UserNotification Authorization access_token IsRead ReadOn"},{"u":"/docs/adr/024-push-notifications.html#related","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox dual-dispatch path, which is distinct: that carries service-to-service integration events, this carries user-facing notifications), ADR-004 (the /hubs…","i":"MMCA.ADC.Notification.Service SendPushNotificationHandler NullNativePushSender Http1AndHttp2 access_token Http2"},{"u":"/docs/adr/024-push-notifications.html#revision-2026-08-07","d":"ADR-024: Two-Channel User Notifications (Transient SignalR Push + Durable Inbox)","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Records transactional email, a delivery path the channel model above never mentions. The decision is unchanged: this closes a documentation gap so the asymmetry reads as…","i":"OrderPaymentFailedSagaHandler SendPushNotificationHandler IPushNotificationSender ILiveChannelPublisher IPushDeviceRegistrar IDomainEventHandler AddInfrastructure INativePushSender OrderPaidHandler PushNotification UserNotification SmtpEmailSender"},{"u":"/docs/adr/025-startup-warmup-readiness.html","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records"},{"u":"/docs/adr/025-startup-warmup-readiness.html#status","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27). Revised 2026-07-28: /health/ready now excludes optional-tagged checks as well as live-tagged ones (see Decision), and the absence of a warm-up timeout was…","i":"optional live"},{"u":"/docs/adr/025-startup-warmup-readiness.html#context","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Context","x":"On the Azure Container Apps Consumption plan a replica that has been idle is CPU-throttled, and a scale-from-zero or scaled-out replica starts cold. The first authenticated…"},{"u":"/docs/adr/025-startup-warmup-readiness.html#decision","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Decision","x":"Ship a small warm-up subsystem in MMCA.Common.Aspire, wired into AddServiceDefaults() so every host gets it. - A readiness gate that starts closed. WarmupReadinessGate…","i":"OpenIdConnectMetadataWarmupTask OperationCanceledException WarmupReadinessHealthCheck MapDefaultEndpoints WarmupHostedService WarmupReadinessGate AddServiceDefaults AddWarmupReadiness IHttpClientFactory MMCA.Common.Aspire TaskTimeoutSeconds cancellationToken"},{"u":"/docs/adr/025-startup-warmup-readiness.html#rationale","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Rationale","x":"- Keep cold replicas out of rotation, briefly. Gating readiness on warm-up means the platform does not send a user request to a replica that is still doing its first handshakes,…","i":"AddServiceDefaults"},{"u":"/docs/adr/025-startup-warmup-readiness.html#trade-offs","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Trade-offs","x":"- A replica can enter rotation not fully warm. The gate is opened in a finally once the Task.WhenAll over every registered task returns, that is, once each task has completed,…","i":"ConfigurationManager TimeoutException stoppingToken Task.WhenAll WaitAsync finally"},{"u":"/docs/adr/025-startup-warmup-readiness.html#related","d":"ADR-025: Startup Warm-Up and Readiness Gating for Cold-Start Mitigation","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the OIDC discovery document the built-in task pre-fetches, and the auth-side view of the same cold-start), ADR-009 (the Polly resilience pipeline that absorbs the lazy…"},{"u":"/docs/adr/026-caching-strategy.html","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","i":"ICacheService"},{"u":"/docs/adr/026-caching-strategy.html#status","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27, amended 2026-07-10, 2026-07-23, 2026-07-25, 2026-08-14). Amended by ADR-077 (2026-08-13): Tier 1's substrate gains a third, opt-in implementation…","i":"OutputCacheEvictionRequested MMCA.Common.OutputCache HybridCacheService ICacheService remarks INCR"},{"u":"/docs/adr/026-caching-strategy.html#context","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Context","x":"The framework needs caching in two distinct places. Inside the application pipeline, query results are memoized and invalidated on mutation (the Caching decorators of ADR-014,…","i":"ICacheInvalidating IQueryCacheable"},{"u":"/docs/adr/026-caching-strategy.html#decision","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Decision","x":"Cache in two tiers, each with its own substrate. - One abstraction. ICacheService (MMCA.Common.Application/Interfaces/ICacheService.cs) exposes GetAsync / SetAsync / RemoveAsync…","i":"builder.Services.AddStackExchangeRedisOutputCache OutputCacheOptions.AddPublicEndpointPolicy MiddlewarePipelineStepNames.OutputCache AbsoluteExpirationRelativeToNow PublicEndpointOutputCachePolicy CacheOptions.DefaultExpiration CacheOptions.DefaultDuration DistributedCacheEntryOptions DistributedCacheService IConnectionMultiplexer LoginProtectionService MemoryDistributedCache"},{"u":"/docs/adr/026-caching-strategy.html#rationale","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Rationale","x":"- One substrate, swapped by environment. Keeping ICacheService as the only thing application code sees lets the deployment decide memory vs distributed. The auto-swap (presence…","i":"ICacheInvalidating IDistributedCache ICacheService"},{"u":"/docs/adr/026-caching-strategy.html#trade-offs","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Trade-offs","x":"- Memory mode is per-replica. In the in-process store each replica caches independently; a scaled-out deployment that did not wire Redis would see cross-replica staleness bounded…","i":"ICacheService.IncrementAsync AddRedisDistributedCache DistributedCacheService StackExchangeRedisCache IConnectionMultiplexer AddOutputCache AddRedisClient RemoveAsync WRONGTYPE NoCache remarks absexp"},{"u":"/docs/adr/026-caching-strategy.html#related","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Related","x":"ADR-014 (the Caching decorators and IQueryCacheable / ICacheInvalidating markers that consume this substrate), ADR-019 (output caching as the anonymous-traffic lever, and…","i":"RegisterUpcastedIntegrationEventConsumer OutputCacheEvictionRequested LoginProtectionService HybridCacheService ICacheInvalidating IQueryCacheable IncrementAsync ICacheService WRONGTYPE TEvent"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-07-24","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Three substrate corrections from a code review. 1. An optional key namespace (Cache:KeyPrefix). Services sharing one cache instance also share one keyspace, and nothing stopped…","i":"RedisCacheOptions.InstanceName ICacheService.IncrementAsync DistributedCacheService EvictionReason.Replaced KeyedSemaphoreStripe RemoveByPrefixAsync MemoryCacheService IMemoryCache InstanceName CacheKey INCR"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-07-25","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"An audit against the code. No behavior changed; the ADR text did. 1. The IncrementAsync entry above was wrong. It described a Redis INCR override. There is no such override, and…","i":"DistributedCacheService StackExchangeRedisCache IncrementAsync AddCaching INCR"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-07-28","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-07-28)","x":"Line anchors only, re-verified against the current source. No decision, no behavior, and no substantive prose changed. 1. Tier 2. Store Catalog's…","i":"AddStackExchangeRedisOutputCache DistributedCacheService MMCA.Common.API ICacheService AddCaching"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-01","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"Line anchors only, re-verified against the current source. No decision, no behavior, and no substantive prose changed. 1. AddCaching. Now at…","i":"AddStackExchangeRedisOutputCache DistributedCacheService RemoveByPrefixAsync ScanAndDeleteAsync IncrementAsync AddCaching remarks returns"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-07","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Line anchors only, re-verified against the current source. No decision, no behavior, and no substantive prose changed. 1. AddCaching. Now at…","i":"AddStackExchangeRedisOutputCache DistributedCacheService MemoryDistributedCache TimeSpan.FromSeconds MemoryCacheService IDistributedCache MMCA.Common.API CacheOptions AddCaching"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-13","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-13)","x":"Tier 1 is amended by ADR-077, which is where the decision and its trade-offs are recorded. The three points that change the reading of this record: 1. A third substrate, opted…","i":"Microsoft.Extensions.Caching.Hybrid DistributedCacheService StackExchangeRedisCache AddCommonHybridCache HybridCacheService MemoryCacheService IDistributedCache IncrementAsync AddCaching WRONGTYPE prefix INCR"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-14","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-14)","x":"One substrate correction plus a line-anchor re-verification. No decision and no behavior changed. 1. The 30-second default now has a named home, CacheOptions.DefaultDuration.…","i":"AddStackExchangeRedisOutputCache AbsoluteExpirationRelativeToNow CacheOptions.DefaultDuration DistributedCacheEntryOptions WebApplicationExtensions.cs AddRedisDistributedCache DistributedCacheService HybridCacheEntryOptions TimeSpan.FromSeconds app.UseOutputCache HybridCacheService DefaultExpiration"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-18","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Every previous revision moved Tier 1. This one moves Tier 2, and it is the first change to the output-cache edge since ADR-040. Tier 2 as decided here is per-process by…","i":"RegisterUpcastedIntegrationEventConsumer RegisterOutputCacheEvictionConsumer RegisterIntegrationEventConsumer AddOutputCacheEvictionHandler OutputCacheEvictionRequested OperationCanceledException OutputCacheEvictionHandler BestEffort.ExecuteAsync MMCA.Common.OutputCache MMCA.Common.BestEffort cache.eviction.failed registerFaultConsumer"},{"u":"/docs/adr/026-caching-strategy.html#revision-2026-08-23","d":"ADR-026: Two-Tier Caching: a Swappable ICacheService Substrate plus an HTTP Output-Cache Edge","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"One correction of substance plus a line-anchor re-verification. No decision and no behavior changed. 1. The counter trade-off now names the contradiction a reader will hit.…","i":"RegisterUpcastedIntegrationEventConsumer MiddlewarePipelineStepNames.OutputCache RegisterOutputCacheEvictionConsumer BaseIntegrationEvent.SchemaVersion AddStackExchangeRedisOutputCache OutputCacheEvictionRequested DistributedCacheService app.UseOutputCache IncrementAsync UseOutputCache ICacheService AddCaching"},{"u":"/docs/adr/027-multi-locale-i18n.html","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records"},{"u":"/docs/adr/027-multi-locale-i18n.html#status","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27, amended 2026-07-02, 2026-07-03, 2026-07-09, and 2026-07-29; corrected 2026-08-01: the pseudo-locale CI gate is required on all three browser engines, and…"},{"u":"/docs/adr/027-multi-locale-i18n.html#context","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Context","x":"ADR-011 recorded single-locale (en-US) as a deliberate, revisitable non-goal and sketched what re-introducing i18n would entail. That revisit has now happened: the framework adds…","i":"InteractiveAuto Error Code"},{"u":"/docs/adr/027-multi-locale-i18n.html#decision","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Decision","x":"1. Supported cultures are an explicit allowlist: en-US (default) + es. Adding a locale is adding a .es.resx sibling set and one allowlist entry, not new infrastructure. 2.…","i":"MmcaCultureBootstrap.SetBrowserCultureAsync ErrorHttpMapping.BuildErrorsExtension DomainInvariantViolationException CultureInfo.DefaultThreadCurrent LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture SupportedCultures.ResolveClosest ApiControllerBase.HandleFailure ResourceTranslationsAreComplete SupportedCultures.PseudoLocale CookieRequestCultureProvider CultureInfo.InvariantCulture"},{"u":"/docs/adr/027-multi-locale-i18n.html#rationale","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Rationale","x":"- Keying error localization on the existing Error.Code is the cheapest correct extension point. The codes are already stable and already cross the wire; localizing at the edge…","i":"ResourcesPath Error.Code resx"},{"u":"/docs/adr/027-multi-locale-i18n.html#trade-offs","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Every view and every user-facing message is touched: a large, mostly mechanical sweep, accepted as the cost ADR-011 always named. - WASM Spanish formatting needs ICU…","i":"InvariantGlobalization ResxMudLocalizer MudTranslations BlazorWebView MudLocalizer"},{"u":"/docs/adr/027-multi-locale-i18n.html#related","d":"ADR-027: Multi-Locale Internationalization (Supersedes ADR-011)","k":"Architecture Decision Records","t":"Related","x":"ADR-011 (superseded), ADR-013 (the Error.Code this localizes on), ADR-015 (the i18n gates now live here: the MA0076 culture-less formatting build gate and the…","i":"ResourceTranslationsAreComplete BlazorWebView Error.Code MA0076"},{"u":"/docs/adr/028-dark-theme-mode.html","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records"},{"u":"/docs/adr/028-dark-theme-mode.html#status","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27; revised 2026-07-15)."},{"u":"/docs/adr/028-dark-theme-mode.html#context","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Context","x":"MMCATheme (MMCA.Common.UI/Theme/MMCATheme.cs) has always defined a complete, brand-tuned PaletteDark alongside PaletteLight, but MudThemeProvider was hard-wired to light: no…","i":"MudThemeProvider InteractiveAuto PaletteLight PaletteDark IsDarkMode MMCATheme ref"},{"u":"/docs/adr/028-dark-theme-mode.html#decision","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Decision","x":"1. Bind the existing theme. The shared MainLayout renders a single component (MMCA.Common.UI/Layout/MainLayout.razor:14), which owns the four Mud providers plus the Day/Dark…","i":"ThemeService.InitializeAsync User.PreferredCulture User.PreferredTheme MMCATheme.Instance MmcaThemeProviders OnAfterRenderAsync InteractiveServer systemPrefersDark window.matchMedia MudThemeProvider MMCA.Common.UI ThemeService"},{"u":"/docs/adr/028-dark-theme-mode.html#rationale","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Rationale","x":"- Reusing the i18n cookie/profile machinery means one persistence model for both user preferences, instead of two subtly different ones. Theme and locale are the same shape of…","i":"BrandColorTokenTests"},{"u":"/docs/adr/028-dark-theme-mode.html#trade-offs","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Trade-offs","x":"- The same FOUC hazard as locale is not yet closed for theme. The SSR data-theme/inline-script read is unimplemented (Decision 3), so the first paint can briefly flash the wrong…","i":"MainLayout User"},{"u":"/docs/adr/028-dark-theme-mode.html#related","d":"ADR-028: Day/Dark Theme Mode","k":"Architecture Decision Records","t":"Related","x":"ADR-027 (shares the cookie source-of-truth and the User preference migration, and is the model for the theme no-flash SSR bootstrap that is not yet wired), ADR-022 (the SSR…","i":"User"},{"u":"/docs/adr/029-authentication-brute-force-protection.html","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#status","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27). Updated 2026-07-02 (the check/increment/reset call sequence was hoisted into AuthenticationServiceBase ; the adoption note and the \"convention the consumer…","i":"AuthenticationServiceBase TUser"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#context","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Context","x":"ADR-019's global rate limiter is authenticated-only: it caps requests per authenticated principal and deliberately exempts anonymous traffic. The highest-value anonymous attack…","i":"RateLimitPolicyAuthIp AuthControllerBase"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#decision","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Decision","x":"Provide a framework ILoginProtectionService (MMCA.Common.Application.Auth) with a single implementation LoginProtectionService (MMCA.Common.Infrastructure.Auth), registered…","i":"RegistrationRateLimitWindowMinutes CheckRegistrationRateLimitAsync IncrementRegistrationCountAsync MMCA.Common.Infrastructure.Auth ICacheService.IncrementAsync IncrementFailedAttemptsAsync MaxRegistrationsPerIpPerHour MMCA.Common.Application.Auth FailedAttemptWindowMinutes AuthenticationServiceBase ResetFailedAttemptsAsync DistributedCacheService"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#rationale","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Rationale","x":"- Complements ADR-019 rather than duplicating it. ADR-019 carries two limiter layers and this is the third on top of them: its global limiter caps authenticated throughput per…","i":"Result"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#trade-offs","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Trade-offs","x":"- Cache-scoped state weakens under scale-out without Redis. In memory mode the counters are per-replica and evaporate on restart, so a multi-replica deployment that did not wire…","i":"AuthenticationServiceBase ILoginProtectionService AuthenticationService TUser"},{"u":"/docs/adr/029-authentication-brute-force-protection.html#related","d":"ADR-029: Authentication Brute-Force Protection and Registration Throttling","k":"Architecture Decision Records","t":"Related","x":"ADR-019 (the layered limiter: an authenticated-only global cap that exempts this anonymous surface, plus the per-IP auth-ip window that now sits on the same two endpoints),…","i":"ICacheService Result Error"},{"u":"/docs/adr/030-startup-sole-migrator.html","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records"},{"u":"/docs/adr/030-startup-sole-migrator.html#status","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27)."},{"u":"/docs/adr/030-startup-sole-migrator.html#context","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Context","x":"Under database-per-service (ADR-006), each service owns its own database and its own migrations project, so something must apply pending migrations on every deploy. The…","i":"ApplicationSettings.DatabaseInitStrategy DatabaseInitializationExtensions EnsureCreated"},{"u":"/docs/adr/030-startup-sole-migrator.html#decision","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Decision","x":"In Azure Container Apps, every service host runs ApplicationSettingsDatabaseInitStrategy = Migrate in production and is the sole migrator of its own database: it applies its…","i":"ApplicationSettings__DatabaseInitStrategy __EFMigrationsHistory DatabaseInitStrategy MigrateAsync minReplicas deploy.yml migrations database Migrate dotnet sqlcmd update"},{"u":"/docs/adr/030-startup-sole-migrator.html#rationale","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Rationale","x":"- One migrator, one mechanism. The code that owns the schema applies the schema; there is no second tool to keep in lockstep and no ordering race between a deploy step and…","i":"__EFMigrationsHistory"},{"u":"/docs/adr/030-startup-sole-migrator.html#trade-offs","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Auto-migrate-in-production is what \"None\" exists to prevent. An unintended or destructive migration would ship itself on the next deploy. The apps accept this; the build-time…","i":"minReplicas"},{"u":"/docs/adr/030-startup-sole-migrator.html#related","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Related","x":"ADR-006 (database-per-service: why each service owns and migrates its own database), ADR-025 (readiness gating keeps traffic off a still-migrating replica), ADR-009 (RTO/RPO +…"},{"u":"/docs/adr/030-startup-sole-migrator.html#revision-2026-08-07","d":"ADR-030: Each Service Self-Applies Its Migrations at Startup (Sole Migrator)","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"The sole-migrator decision extends to seed data: the same startup owner that applies the schema also runs the module seeders, in the same call, on the same boot. The Decision…","i":"moduleLoader.SeedAllAsync ModuleLoader.SeedAllAsync ConferenceModuleDbSeeder InitializeDatabaseAsync __EFMigrationsHistory DatabaseInitStrategy builder.Build IModuleSeeder ExistsAsync DbSeeder Guid int"},{"u":"/docs/adr/031-feature-flag-management.html","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records"},{"u":"/docs/adr/031-feature-flag-management.html#status","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-27). Revised 2026-08-18 (a targeting-context accessor is now registered, so the built-in Targeting and Percentage filters give consistent per-user bucketing…"},{"u":"/docs/adr/031-feature-flag-management.html#context","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Context","x":"The apps need to decouple release from deploy: ship code dark, flip a kill switch, or roll a feature out to a percentage of users without a redeploy. A flag has to be enforceable…","i":"FeatureGate"},{"u":"/docs/adr/031-feature-flag-management.html#decision","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Decision","x":"Standardize on Microsoft.FeatureManagement, configured from the \"FeatureManagement\" configuration section and registered once in AddAPI (services.AddFeatureManagement() +…","i":"ApiControllerBase.HandleFailure Microsoft.FeatureManagement.Mvc IFeatureManager.IsEnabledAsync services.AddFeatureManagement FeatureGateCommandDecorator Microsoft.FeatureManagement FeatureGateQueryDecorator Error.NotFoundError ConferenceFeatures EngagementFeatures ErrorType.NotFound CatalogFeatures"},{"u":"/docs/adr/031-feature-flag-management.html#rationale","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Rationale","x":"- Release decoupled from deploy. A kill switch or a percentage rollout becomes a configuration change, not a code change: the central reason feature management exists. - Two…"},{"u":"/docs/adr/031-feature-flag-management.html#trade-offs","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Trade-offs","x":"- The two enforcement points must agree. A flag gated on the controller but not the handler (or vice versa) is a half-protected feature; no fitness rule asserts both are wired,…","i":"IsEnabledAsync"},{"u":"/docs/adr/031-feature-flag-management.html#revision-2026-08-18","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Progressive rollout is now usable, because the targeting context exists. The Decision above listed the Percentage / TimeWindow / Targeting filters as \"available\", and the last…","i":"CurrentUserTargetingContextAccessor FeatureGateCommandDecorator ITargetingContextAccessor featureGated.FeatureName AddHttpContextAccessor IHttpContextAccessor ICurrentUserService ClaimTypes.Role IFeatureManager IsEnabledAsync Identity.Name WithTargeting"},{"u":"/docs/adr/031-feature-flag-management.html#related","d":"ADR-031: Config-Driven Feature Flags with Dual-Surface Enforcement","k":"Architecture Decision Records","t":"Related","x":"ADR-014 (the decorator pipeline whose outermost slot FeatureGate fills, and the ordering that puts it first, now with Authorization registered directly inside it so a disabled…","i":"FeatureGate Groups Result Error"},{"u":"/docs/adr/032-password-hashing.html","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records"},{"u":"/docs/adr/032-password-hashing.html#status","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-29, adoption note revised 2026-07-06, registration note revised 2026-08-01, call-site hoist recorded 2026-08-23)."},{"u":"/docs/adr/032-password-hashing.html#context","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Context","x":"Identity stores a credential as a (salt, hash) pair, never plaintext. The framework needs one canonical hasher that every consuming Identity flow shares, so the key-derivation…"},{"u":"/docs/adr/032-password-hashing.html#decision","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Decision","x":"Provide a single IPasswordHasher (MMCA.Common.Application.Interfaces.Infrastructure, IPasswordHasher.cs:6) with one implementation PasswordHasher…","i":"MMCA.Common.Application.Interfaces.Infrastructure CryptographicOperations.FixedTimeEquals RandomNumberGenerator.GetBytes IdentityModuleDbSeederBase AuthenticationServiceBase ChangePasswordHandlerBase Rfc2898DeriveBytes.Pbkdf2 HashAlgorithmName.SHA512 IdentityModuleDbSeeder AuthenticationService ChangePasswordHandler LegacyHmacSaltSize"},{"u":"/docs/adr/032-password-hashing.html#rationale","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Rationale","x":"- One framework-owned primitive, not per-app crypto. Putting the algorithm, work factor, salt size, and comparison in a single shared type means a future hardening (raising…","i":"IsLegacy"},{"u":"/docs/adr/032-password-hashing.html#trade-offs","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Trade-offs","x":"- The legacy branch is a permanent correctness dependency that looks deletable. Its load-bearing role is invisible from the method body alone, so it is the single most…","i":"VerifyPassword Iterations"},{"u":"/docs/adr/032-password-hashing.html#related","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (cross-service JWT / JWKS authentication: the hasher gates credential verification that issues the tokens that ADR-004 then validates across services), ADR-005…","i":"EncryptedStringConverter"},{"u":"/docs/adr/032-password-hashing.html#revision-2026-08-23","d":"ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"The decision is unchanged: one framework-owned IPasswordHasher, PBKDF2-HMAC-SHA512 for new passwords, salt-length dispatch on verification. What changed is where the last two…","i":"IdentityModuleDbSeederBase ChangePasswordHandlerBase IdentityModuleDbSeeder AuthenticationService ChangePasswordHandler IPasswordHasher ChangePassword VerifyPassword HashPassword HandlerName CreateUser Accounts"},{"u":"/docs/adr/033-resource-ownership-authorization.html","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records"},{"u":"/docs/adr/033-resource-ownership-authorization.html#status","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02, revised 2026-07-25)."},{"u":"/docs/adr/033-resource-ownership-authorization.html#context","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Context","x":"ADR-020 added a permission (capability) layer over RBAC: it answers \"what may this role do\", resolving a role to a permission so an endpoint can require a capability instead of a…","i":"OwnerOrAdminFilter GET"},{"u":"/docs/adr/033-resource-ownership-authorization.html#decision","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Decision","x":"Provide a row/resource-level ownership axis in MMCA.Common.API (the Authorization folder), with two enforcement points keyed on the caller's owner claim (customerid by default)…","i":"ShoppingCartsController.GetAllForLookupAsync ShoppingCartByCustomerSpecification ShoppingCartsController.GetAllAsync AggregateRootEntityControllerBase CustomersController.CreateAsync CustomersController.GetAllAsync OrdersByCustomerSpecification GetOwnershipSpecification OwnerOrAdminFilterOptions ICurrentUserService.Role OwnershipHelper.IsAdmin settings.OwnerClaimType"},{"u":"/docs/adr/033-resource-ownership-authorization.html#rationale","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Rationale","x":"- Reject-one and filter-many are genuinely two mechanisms. A single-resource route has an id to compare, so a short action filter that 403s on a mismatch is the cheapest correct…","i":"IEntityQueryService Specification Criteria TEntity And TId"},{"u":"/docs/adr/033-resource-ownership-authorization.html#trade-offs","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in per controller/handler. Neither point is automatic: a controller that forgets the [ServiceFilter] or omits the ownership spec from a query leaks across customers, the…","i":"OwnerOrAdminFilter ServiceFilter customer_id null"},{"u":"/docs/adr/033-resource-ownership-authorization.html#related","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Related","x":"ADR-020 (the role/permission RBAC layer this complements, and whose explicit 020-permission-based-authorization.md:78 scope-out this fills), ADR-034 (the generic entity query…","i":"IEntityQueryService Specification ForbidResult Result"},{"u":"/docs/adr/033-resource-ownership-authorization.html#revision-2026-07-25","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"An audit against the code. No behavior changed; the ADR text did. 1. The per-mutation check's failure shape was described as one branch, and it is two. ValidateOwnershipAsync was…","i":"ICurrentUserService.Role ValidateOwnershipAsync OwnerOrAdminFilter AllowMissingOwner OrdersController Error.Forbidden"},{"u":"/docs/adr/033-resource-ownership-authorization.html#revision-2026-08-01","d":"ADR-033: Resource-Ownership Authorization (Row-Level + Action Filter)","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"Anchor-only correction. No behavior changed; OrdersController was refactored (a constructor parameter added, GetOwnershipSpecification() and the IsAdmin property extracted,…","i":"GetOwnershipSpecification ValidateOwnershipAsync OrdersController Error.Forbidden Error.NotFound IsAdmin"},{"u":"/docs/adr/034-generic-entity-query-layer.html","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/034-generic-entity-query-layer.html#status","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-06-30). Amended (2026-07-23): the filter strategy registry now also covers long/long? via LongFilterStrategy. Amended (2026-07-25): the keyed by-id fast path is…","i":"TryGetFastPathIncludes LongFilterStrategy long"},{"u":"/docs/adr/034-generic-entity-query-layer.html#context","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Context","x":"Every module exposes many entities, and most of them need the same read and write surface: list, page, look up for a dropdown, fetch by id, create, delete. Hand writing a…"},{"u":"/docs/adr/034-generic-entity-query-layer.html#decision","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Decision","x":"Give every entity a generic REST resource surface and a bounded dynamic query contract, supplied by two controller bases over a shared query pipeline. 1. Generic read controller.…","i":"EntityQueryPipeline.MaxUnboundedResultLimit QueryFieldService.ApplyFieldSelection QueryFilterService.RegisterStrategy IApplicationSettings.MaxPageSize QueryFilterService.ApplyFilters QueryFieldService.ApplySorting MaxUnboundedResultLimit QueryFilterModelBinder INavigationPopulator EntityQueryPipeline SupportedOperators IFilterStrategy"},{"u":"/docs/adr/034-generic-entity-query-layer.html#rationale","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Rationale","x":"- Write the resource surface once, inherit it everywhere. The verbs, routes, filter contract, pagination, and header are defined once on the two bases; a concrete controller…","i":"QueryFilterService.ValidateFilters MaxUnboundedResultLimit DTOToEntityPropertyMap INavigationPopulator DTOMapper.MapToDTOs SupportedOperators IEntityDTOMapper IFilterStrategy MaxPageSize"},{"u":"/docs/adr/034-generic-entity-query-layer.html#trade-offs","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- The wire contract tracks the entity model. Filterable, sortable, and projectable surface is the entity's property set. A model change is an API change unless mediated by the…","i":"QueryFilterService.ValidateFilters MaxUnboundedResultLimit DTOToEntityPropertyMap IFilterStrategy virtual"},{"u":"/docs/adr/034-generic-entity-query-layer.html#related","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-001 (manual DTO mapping: the generic controllers project through IEntityDTOMapper), ADR-002 (navigation populators: the unsupported-include path), ADR-013 (Result pattern at…","i":"IEntityDTOMapper HandleFailure result.Errors Idempotent"},{"u":"/docs/adr/034-generic-entity-query-layer.html#revision-2026-07-24","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Filter and pagination corrections from a code review. The contract shape is unchanged; these close cases where the engine widened a result set instead of narrowing it, or…","i":"IFilterStrategy.CanParseValue PaginationMetadata.PageSize MaxUnboundedResultLimit DTOToEntityPropertyMap Filter.Value.Invalid ValidateFilters FirstOrDefault TotalItemCount ApplyFilters GetByIdAsync int.MaxValue includeFKs"},{"u":"/docs/adr/034-generic-entity-query-layer.html#revision-2026-07-25","d":"ADR-034: Generic Entity Controllers with a Dynamic Query Contract","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"Citation maintenance from an ADR audit. No decision or behavior changed; the source anchors above were rebased to their current declaration and call-site lines. 1. The fast-path…","i":"IsPrimaryKeyOnlyLookup TryGetFastPathIncludes"},{"u":"/docs/adr/035-optimistic-concurrency.html","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records"},{"u":"/docs/adr/035-optimistic-concurrency.html#status","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02). Amended 2026-07-16: a child-entity overload of SetOriginalRowVersion was added (see Decision). Revised 2026-08-18: the same token gains an HTTP-native…","i":"SetOriginalRowVersion IConcurrencyAware SupportsIfMatch GetByIdAsync RowVersion ETag"},{"u":"/docs/adr/035-optimistic-concurrency.html#context","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Context","x":"Every mutable aggregate in the framework is edited through a load-modify-save handler: the update use case fetches the tracked entity, applies the request, and calls…","i":"SaveChangesAsync Conflict"},{"u":"/docs/adr/035-optimistic-concurrency.html#decision","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Decision","x":"Give every auditable entity a database-managed RowVersion concurrency token, round-trip it through the client on updates, and stamp the client's last-seen value as EF's original…","i":"MMCA.Common.Domain.Interfaces.IRowVersioned IWriteRepository.SetOriginalRowVersion ConcurrencyConventionTestsBase MMCA.Store.Architecture.Tests DbUpdateConcurrencyException MMCA.ADC.Architecture.Tests AddRowVersionToAllEntities ConfigureConcurrencyTokens DbUpdateExceptionHandler SetOriginalRowVersion AuditableBaseEntity ErrorType.Conflict"},{"u":"/docs/adr/035-optimistic-concurrency.html#rationale","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Rationale","x":"- Database-managed token over a hand-maintained version field. A SQL Server rowversion auto-increments on the server on every write; no domain code sets or reads it (the setter…","i":"DbUpdateExceptionHandler SetOriginalRowVersion DbUpdateException rowversion WHERE"},{"u":"/docs/adr/035-optimistic-concurrency.html#trade-offs","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in at the caller, not just the type. A null or empty RowVersion skips the check, so a client that never echoes the token still gets last-write-wins. The fitness function…","i":"AddRowVersionToAllEntities DbUpdateExceptionHandler IsConcurrencyToken DbUpdateException UpdateRequest rowversion RowVersion byte"},{"u":"/docs/adr/035-optimistic-concurrency.html#revision-2026-08-18","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"This record chose a body round-trip: the client echoes RowVersion on the update request. HTTP has had a standard way to say the same thing since long before this framework, ETag…","i":"RewriteConflictToPreconditionFailed EntityControllerBase.GetByIdAsync UpdateRequestsAreConcurrencyAware DbUpdateConcurrencyException DbUpdateExceptionHandler SupportsIfMatchAttribute ServiceFilterAttribute SetOriginalRowVersion IAsyncActionFilter SetConcurrencyETag HttpContext.Items IConcurrencyAware"},{"u":"/docs/adr/035-optimistic-concurrency.html#related","d":"ADR-035: Optimistic Concurrency via RowVersion Round-Trip","k":"Architecture Decision Records","t":"Related","x":"ADR-017 (HTTP request idempotency, which dedups retries of the same request, the mirror-image concern to two distinct edits racing here, and whose own 2026-08-18 revision adds…","i":"AuditableBaseEntity GetByIdAsync RowVersion"},{"u":"/docs/adr/036-external-oauth-login.html","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records"},{"u":"/docs/adr/036-external-oauth-login.html#status","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-02, migration attribution corrected 2026-07-06, native-callback redirect branch added 2026-07-17 per ADR-043, email-verified account-takeover guard before…"},{"u":"/docs/adr/036-external-oauth-login.html#context","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Context","x":"The framework's Identity story so far is entirely first-party: a user registers with an email and password, the credentials are hashed (ADR-032), and Identity mints its own RS256…","i":"AddPermissions User"},{"u":"/docs/adr/036-external-oauth-login.html#decision","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Decision","x":"Add an opt-in external-login path that federates Google/GitHub sign-in at the edge and immediately exchanges the external identity for the app's own local JWT pair, linking the…","i":"IAuthenticationService.ExternalLoginAsync OAuthControllerBase.CompleteAsync AddExternalLoginProviderFields Auth.ExternalLoginNotSupported Auth.ExternalEmailNotVerified ConfigurationOAuthUISettings Auth.ExternalEmailInvalid User.LinkExternalProvider AddExternalAuthProviders AddCommonAuthentication AuthenticationResponse IAuthenticationService"},{"u":"/docs/adr/036-external-oauth-login.html#rationale","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Rationale","x":"- Terminate federation at the edge, keep one internal identity. Exchanging the external principal for a local JWT the moment the callback returns means every downstream concern…","i":"ExternalLoginAsync ClientId POST User GET"},{"u":"/docs/adr/036-external-oauth-login.html#trade-offs","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in per app, and easy to half-wire. The flow needs four cooperating pieces (scheme registration, the controller subclass, the service override, and the OAuthUIBaseUrl…","i":"Auth.ExternalLoginNotSupported Auth.ExternalEmailNotVerified IExternalLoginEmailVerifier OAuth__UIBaseUrl IsExternalLogin email_verified ExternalLogin LoginProvider ProviderKey ClientId User"},{"u":"/docs/adr/036-external-oauth-login.html#related","d":"ADR-036: External OAuth Login (Federated Google/GitHub) with Local-JWT Exchange","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the RS256/JWKS token this flow exchanges the external identity for, and validates everywhere after), ADR-022 (the browser cookies that carry the resulting session),…","i":"User.Anonymize CompleteAsync"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#status","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-06; revised 2026-07-24, 2026-07-25, 2026-08-15, 2026-08-18). Revised 2026-08-18: the versioned-envelope converter is no longer unpublished, it is included in…"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#context","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Context","x":"Transparent database encryption (TDE) protects the data files as a whole, but it decrypts transparently for anyone who can query the database, so a leaked backup restored on a…"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#decision","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Decision","x":"Provide a single framework-owned EF Core value converter that transparently encrypts string columns at rest with authenticated encryption, applied per property in an entity…","i":"MMCA.Common.Infrastructure.Persistence.Encryption ArgumentNullException.ThrowIfNull RandomNumberGenerator.GetBytes EncryptedStringConverterTests MMCA.Common.Infrastructure EncryptedStringConverter CryptographicException IReadOnlyDictionary ArgumentException FromBase64String FrozenDictionary AesGcm.Decrypt"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#rationale","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Rationale","x":"- Authenticated, not merely confidential. AES-GCM binds a 128-bit tag to the ciphertext (EncryptedStringConverter.cs:81, :201), so a tampered or truncated value fails to decrypt…","i":"AesGcm.Decrypt string"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#trade-offs","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Latent today, proven by tests rather than production. The plumbing is complete and unit-tested, but no entity configuration wires it, so the encrypt/decrypt round-trip, the…","i":"EncryptedStringConverterTests CryptographicException HasConversion byte"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#related","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (soft-delete vs erasure: the other sensitive-data control, which names this converter as the mechanism for erasure fields that must stay retrievable,…"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#revision-2026-07-24","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Documented a constraint the converter always had but did not state: the ciphertext is non-deterministic. Every write uses a fresh random nonce, which is the correct property for…","i":"Email Where"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#revision-2026-07-25","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Revision (2026-07-25)","x":"Documentation-only correction, no behavior change. Item 1 of the Decision still illustrated the converter with builder.Property(e = e.Email), contradicting the 2026-07-24…","i":"EncryptedStringConverter.cs SocialSecurityNumber builder.Property e.Email"},{"u":"/docs/adr/037-field-level-encryption-at-rest.html#revision-2026-08-15","d":"ADR-037: Field-Level Encryption at Rest (AES-256-GCM EF Converter)","k":"Architecture Decision Records","t":"Revision (2026-08-15)","x":"Behavior change, not a documentation correction. The stored layout is now a versioned envelope: Base64 of [key version (1)] [nonce (12)] [ciphertext (N)] [tag (16)] rather than…","i":"SaveChanges ciphertext DbContext version nonce main key tag"},{"u":"/docs/adr/038-supply-chain-provenance.html","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records"},{"u":"/docs/adr/038-supply-chain-provenance.html#status","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-06; revised 2026-07-21)."},{"u":"/docs/adr/038-supply-chain-provenance.html#context","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common is a published framework: it packs its NuGet packages and pushes them to GitHub Packages on every v tag (release.yml:3-5), where the two production apps and the…","i":"Directory.Build.props nuget.config"},{"u":"/docs/adr/038-supply-chain-provenance.html#decision","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Decision","x":"Treat supply-chain integrity as a set of build-gating controls, the same invariant-over-discipline posture ADR-015 applies to architecture rules. Four controls, each a hard gate:…","i":"SQLitePCLRaw.bundle_e_sqlite3 RestorePackagesWithLockFile MMCA.Common.Infrastructure Directory.Packages.props Directory.Build.props TreatWarningsAsErrors packageSourceMapping NuGetAuditSuppress packages.lock.json MMCA.Common.slnx nuget.config NuGetAudit"},{"u":"/docs/adr/038-supply-chain-provenance.html#rationale","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Rationale","x":"- Provenance is a gate, not a document. A hard-failing SBOM step means the bill of materials cannot silently go missing on a release: the artifact is produced or the release…","i":"Directory.Build.props NuGetAuditSuppress dotnet list"},{"u":"/docs/adr/038-supply-chain-provenance.html#trade-offs","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The SBOM is generated and archived, not yet signed or attested. The gate proves a bill of materials exists for each release (release.yml:58); it does not add cryptographic…","i":"NuGetAuditSuppress nuget.config dotnet list"},{"u":"/docs/adr/038-supply-chain-provenance.html#related","d":"ADR-038: Supply-Chain Provenance (SBOM Release Gate + Lock Files + Vulnerability Audit)","k":"Architecture Decision Records","t":"Related","x":"ADR-016 (lockstep versioning + the MassTransit-v8 license pin; this record extends dependency governance from versioning and licensing into supply-chain provenance and…"},{"u":"/docs/adr/039-live-channel-push.html","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records"},{"u":"/docs/adr/039-live-channel-push.html#status","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-09)."},{"u":"/docs/adr/039-live-channel-push.html#context","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Context","x":"Conference-day features (live polls, session Q&A, live result counters) need sub-second fan-out of small events to whoever is looking at a page right now. The existing…"},{"u":"/docs/adr/039-live-channel-push.html#decision","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Decision","x":"One realtime transport, two publisher boundaries: - NotificationHub stays the single hub and gains its first client-invokable methods: JoinChannel / LeaveChannel map the calling…","i":"PushNotificationSettings.ChannelKeyPattern SignalRLiveChannelPublisher NullLiveChannelPublisher IPushNotificationSender NotificationHubService ILiveChannelPublisher AddPushNotifications ReceiveChannelEvent LeaveChannelAsync JoinChannelAsync NotificationHub OnChannelEvent"},{"u":"/docs/adr/039-live-channel-push.html#rationale","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Rationale","x":"- One WebSocket per client keeps connection management, token refresh, reconnect, and backplane behavior in one place; channel membership is a property of the existing…","i":"IMessageBus"},{"u":"/docs/adr/039-live-channel-push.html#trade-offs","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Ephemeral means lossy: a client that connects after an event was published never sees it. Features must treat channel events as cache-invalidation hints over fetchable state,…","i":"NotificationCallback"},{"u":"/docs/adr/039-live-channel-push.html#revision-2026-07-24","d":"ADR-039: Live channel push (ephemeral events over the notification hub)","k":"Architecture Decision Records","t":"Revision (2026-07-24)","x":"Two corrections from a code review; the best-effort, per-session-ordered decision is unchanged. 1. Broadcasts are enqueued after commit, not during the command. CastVoteHandler…","i":"BoundedChannelFullMode.DropOldest SessionQuestionUpvoteChanged LivePollVoteChanged ToggleUpvoteHandler CastVoteHandler DroppedCount itemDropped TryWrite"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#status","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-10). Amended (2026-07-10): explicit query-string variance parity with the built-in default policy (the initial release accidentally dropped it, collapsing every…","i":"OutputCacheEvictionRequested ContentEditor SponsorsCache NowNextCache bypassRoles Organizer"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#context","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Context","x":"The framework's read-scaling design leans on ASP.NET Core output caching: anonymous-readable endpoints ([AllowAnonymous] GETs like event/session/speaker catalogs) carry named…","i":"AuthDelegatingHandler BookmarkCountsCache AllowAnonymous Authorization NowNextCache"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#decision","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Decision","x":"MMCA.Common.API ships PublicEndpointOutputCachePolicy, an IOutputCachePolicy that mirrors the built-in default policy with one deliberate difference: it does not disable cache…","i":"ConferenceReadAudience.PrivilegedRoles PublicEndpointOutputCachePolicy DbUpdateConcurrencyException IOutputCachePolicy MMCA.Common.API AllowAnonymous Authorization ContentEditor NowNextCache extension Organizer reference"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#rationale","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Rationale","x":"- The response payload, not the request's auth state, is what determines cacheability. For a user-independent payload, Authorization is noise; refusing to cache on it turns the…","i":"Authorization"},{"u":"/docs/adr/040-authenticated-output-caching-for-public-reads.html#trade-offs","d":"ADR-040: Authenticated output caching for public reads","k":"Architecture Decision Records","t":"Trade-offs","x":"- Consumers must audit which named policies move to AddPublicEndpointPolicy. Policies on permission-gated endpoints (e.g. an organizer dashboard) must NOT move; if such an…","i":"UserSessionBookmarkCacheEvictionHandler RegisterOutputCacheEvictionConsumer AddStackExchangeRedisOutputCache AddOutputCacheEvictionHandler OutputCacheEvictionRequested AddRedisDistributedCache AddPublicEndpointPolicy BookmarkCountsCache IDistributedCache EvictByTagAsync AddOutputCache NowNextCache"},{"u":"/docs/adr/041-observability-and-telemetry.html","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records"},{"u":"/docs/adr/041-observability-and-telemetry.html#status","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-10). Amended (2026-07-23) to document the Telemetry:DisableHttpClientMetrics and Telemetry:DisableRuntimeMetrics cost knobs and to correct the…","i":"MMCA.Common.OutputCache MMCA.Common.BestEffort OutboxProcessor RecordDuration OutboxMetrics OutboxProcess HttpClient finally reason"},{"u":"/docs/adr/041-observability-and-telemetry.html#context","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Context","x":"The framework is a modular monolith whose modules extract into standalone services (ADR-008), so the same telemetry has to make sense whether a request stays in one process or…","i":"HttpClient"},{"u":"/docs/adr/041-observability-and-telemetry.html#decision","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Decision","x":"Standardize telemetry in the shared Aspire service defaults, add framework-specific instrumentation for the CQRS and outbox paths, and expose cost knobs with fail-safe defaults.…","i":"APPLICATIONINSIGHTS_CONNECTION_STRING CqrsMetrics.CommandDuration.Record HttpContext.TraceIdentifier OTEL_EXPORTER_OTLP_ENDPOINT AddOpenTelemetryExporters IsInstrumentationDisabled OutboxPollFilterProcessor outbox.dead_letter.count TraceIdRatioBasedSampler CorrelationIdMiddleware ConfigureOpenTelemetry TryGetTraceSampleRatio"},{"u":"/docs/adr/041-observability-and-telemetry.html#rationale","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Rationale","x":"- Instrument only where auto-instrumentation is blind. The CQRS RED histograms and the outbox dead-letter counter cover the two framework-owned hot paths; everything else (HTTP,…","i":"ParentBased HttpClient true"},{"u":"/docs/adr/041-observability-and-telemetry.html#trade-offs","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Trade-offs","x":"- Custom instrumentation carries a maintenance cost. The Aspire package has no reference to Application or Infrastructure by design, so the meter and activity-source names are…","i":"OutboxProcess ParentBased"},{"u":"/docs/adr/041-observability-and-telemetry.html#revision-2026-08-18","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Two meters and one hop. Two new failure counters, each on its own meter. cache.eviction.failed, tagged cachetag, on MMCA.Common.OutputCache…","i":"GatewayCorrelationMiddleware besteffort.dispatch.failed CorrelationIdMiddleware MMCA.Common.Idempotency MMCA.Common.OutputCache MMCA.Common.BestEffort cache.eviction.failed MMCA.Common.Scheduler MMCA.Common.Broker MMCA.Common.Outbox MMCA.Common.Cqrs Idempotency"},{"u":"/docs/adr/041-observability-and-telemetry.html#related","d":"ADR-041: Observability and Telemetry Strategy","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox whose dead-letter counter and poll-span filtering this defines), ADR-014 (the CQRS decorator pipeline that emits the RED histograms as a byproduct of its…","i":"AddServiceDefaults"},{"u":"/docs/adr/042-device-capability-abstraction.html","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records"},{"u":"/docs/adr/042-device-capability-abstraction.html#status","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-10, amended 2026-07-17, 2026-07-23 and 2026-08-14)."},{"u":"/docs/adr/042-device-capability-abstraction.html#context","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Context","x":"The consumer apps ship the same Blazor component set through three heads: MAUI Blazor Hybrid (Android/iOS/MacCatalyst/Windows), Blazor Server SSR, and WebAssembly. Native device…","i":"builder.Services.AddCommonMauiTokenStorage ITokenStorageService navigator.clipboard navigator.onLine navigator.share MMCA.Common.UI AddUIShared"},{"u":"/docs/adr/042-device-capability-abstraction.html#decision","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Decision","x":"Add a per-capability contract layer to MMCA.Common.UI and a fifteenth package, MMCA.Common.UI.Maui, carrying the native implementations. - One small interface per capability, no…","i":"IExternalLinkService.InterceptsLinks AddBrowserDeviceCapabilities AddDeviceCapabilityDefaults EnforceUIMauiLayerBoundary IConnectivityStatusService AddMauiDeviceCapabilities ILocalNotificationService UseMauiDeviceCapabilities Directory.Packages.props IPushDeviceTokenProvider IPushRegistrationService MauiBackNavigationBridge"},{"u":"/docs/adr/042-device-capability-abstraction.html#rationale","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Rationale","x":"- A god IDeviceCapabilities interface would force every head to implement everything and turn each new capability into a breaking change; per-capability contracts are open/closed…","i":"IDeviceCapabilities AddUIShared"},{"u":"/docs/adr/042-device-capability-abstraction.html#trade-offs","d":"ADR-042: Device Capability Abstraction (MAUI Blazor Hybrid)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A fifteenth package raises release surface: two runners must both succeed for a whole release. Accepted; the publish-maui job is gated by the same tag and SBOM discipline. -…","i":"AddMauiDeviceCapabilities UseMauiDeviceCapabilities MauiExternalAuthBroker AddUIShared IsAvailable IsSupported false"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#status","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-07-28 (the Android https App Links leg is recorded as shipped, the outstanding Android item is restated as the served certificate fingerprint,…","i":"REPLACE_WITH_PLAY_APP_SIGNING_SHA256_FINGERPRINT WebAuthenticatorCallbackActivity MapAppAssociationEndpoints sha256_cert_fingerprints MauiExternalAuthBroker AppAssociationOptions IDeepLinkDispatcher assetlinks.json MMCA.ADC.UI.Web CompleteAsync MainActivity AutoVerify"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#context","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Context","x":"Three mobile flows all need a URL to leave the web world and land inside the MAUI app: 1. Shared links and QR codes. The share sheet and QR codes carry ordinary https web URLs.…","i":"OAuthControllerBase.CompleteAsync IDeepLinkDispatcher WebAuthenticator assetlinks.json CompleteAsync"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#decision","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Decision","x":"- Custom-scheme returnUrl allowlist in the framework. CompleteAsync consults OAuth:AllowedReturnUrlSchemes (a config array, default empty). When the challenge's stashed returnUrl…","i":"IAuthUIService.ExchangeOAuthCodeAsync WebAuthenticatorCallbackActivity ITokenStorageService IDeepLinkDispatcher IExternalAuthBroker Uri.OriginalString CFBundleURLTypes WebAuthenticator assetlinks.json CompleteAsync AutoVerify returnUrl"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#rationale","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Rationale","x":"- Reusing the single-use-code exchange keeps the token-never-in-URL invariant identical across web and native; the only new surface is WHERE the code lands. - A scheme allowlist…"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#trade-offs","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Trade-offs","x":"- The app-facing hostname is baked into store binaries (intent filters, entitlements). The apps currently ride the Azure Container Apps default domain, which changes if the…","i":"appsettings.json EmbeddedResource PublicWebHost"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#revision-2026-07-28","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Revision (2026-07-28)","x":"Correction pass from an ADR audit. No decision or behavior changed; the Status section had the Android leg backwards and the Decision section attributed the token exchange to the…","i":"IAuthUIService.ExchangeOAuthCodeAsync ITokenStorageService.SetTokensAsync MapAppAssociationEndpoints sha256_cert_fingerprints AndroidCertFingerprints BuildSuccessRedirectUrl IDeepLinkDispatcher WebAuthenticator CompleteAsync IntentFilter MainActivity OnNewIntent"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#revision-2026-08-01","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Revision (2026-08-01)","x":"Status pass from an ADR audit. No decision and no behavior changed; the one item the previous revision left open is closed, and the anchor that revision itself introduced had…","i":"MapAppAssociationEndpoints sha256_cert_fingerprints AndroidCertFingerprints AndroidPackageName assetlinks.json ApplicationId Program.cs d5fd0e9"},{"u":"/docs/adr/043-mobile-deep-links-and-native-oauth-callback.html#revision-2026-08-07","d":"ADR-043: Mobile Deep Links, App Association, and the Native OAuth Callback","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Anchor and precision pass from an ADR audit. No decision and no behavior changed. 1. The two Program.cs anchors moved one line. MMCA.ADC commit 886fa189 (PR 100, merged…","i":"app.MapAppAssociationEndpoints AndroidCertFingerprints AppAssociationOptions AndroidPackageName PublicWebHost GetSection Program.cs new"},{"u":"/docs/adr/044-native-push-delivery.html","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records"},{"u":"/docs/adr/044-native-push-delivery.html#status","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-11). Amends ADR-024. The framework pipeline is implemented and inert by default; each consumer switches it on by provisioning a notification hub with platform…","i":"NativePush"},{"u":"/docs/adr/044-native-push-delivery.html#context","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Context","x":"ADR-024 established two notification channels: a durable per-user UserNotification inbox (the source of truth) and a transient SignalR push behind IPushNotificationSender. Both…","i":"IPushNotificationSender UserNotification"},{"u":"/docs/adr/044-native-push-delivery.html#decision","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Decision","x":"- Azure Notification Hubs as the delivery fan-out. One hub abstracts both platforms behind one API, holds the platform credentials outside our code, and its installation model…","i":"INativePushSender.SendToUsersAsync Notification.PushNotifications MauiPushRegistrationService NullPushDeviceTokenProvider SendPushNotificationHandler AddNativePushNotifications AddNotificationControllers AuthUIService.LogoutAsync IPushDeviceTokenProvider IPushRegistrationService PushRegistrationListener IPushDeviceRegistrar"},{"u":"/docs/adr/044-native-push-delivery.html#consequences","d":"ADR-044: Native Push Delivery (the Third Notification Channel)","k":"Architecture Decision Records","t":"Consequences","x":"- Sends fan out per 20-user chunk and per platform: an audience of N users costs ceil(N/20) 2 hub calls. Acceptable at conference scale; a template-based send can consolidate…","i":"SendPushNotificationHandler ceil"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#status","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-11). Records the BR-116 amendment (ADC): avatar photos are IN scope, powered by two new framework extension points. The framework legs are implemented; each…"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#context","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Context","x":"The MAUI capability program (ADR-042) brought MediaPicker/camera within reach, and ADC amended BR-116 to include user avatar photos. That needs binary blob storage (the databases…"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#decision","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Decision","x":"- IFileStorageService (Application): upload-by-blob-name returning the public URI, plus idempotent delete. Default is an unconfigured Null implementation whose uploads fail with…","i":"ImageSharpImageProcessor AddAzureBlobFileStorage IFileStorageService IMediaPickerService ConnectionString IImageProcessor configuration ContainerName FileStorage IsSupported ServiceUri InputFile"},{"u":"/docs/adr/045-managed-file-storage-and-avatars.html#consequences","d":"ADR-045: Managed File Storage and User Avatars","k":"Architecture Decision Records","t":"Consequences","x":"- The avatars container is public-read by design: avatar URLs render in tags on anonymous-visible surfaces without SAS plumbing. The random blob suffix prevents enumeration; the…","i":"DefaultAzureCredential img"},{"u":"/docs/adr/046-http-api-versioning.html","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records"},{"u":"/docs/adr/046-http-api-versioning.html#status","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-08-01 (anonymity is granted by each per-service subclass, not by ServiceInfoControllerBase; corrected the ADR-034 cross-reference, which puts…","i":"ServiceInfoControllerBase AddCommonApiVersioning EntityControllerBase DefaultApiVersion Asp.Versioning"},{"u":"/docs/adr/046-http-api-versioning.html#context","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Context","x":"The framework's REST surface is served by controllers hosted in extracted service processes behind a YARP gateway. As those services evolve, a response shape has to be able to…","i":"Asp.Versioning SchemaVersion v1.0"},{"u":"/docs/adr/046-http-api-versioning.html#decision","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Decision","x":"Standardize one header-based API-versioning setup in MMCA.Common.API, adopt it in every service host through a single registration call, and keep it exercised by a shared fitness…","i":"ApiParameterDescription.ParameterDescriptor ApiParameterDescriptorBackfillProvider ServiceInfoVersioningContractTestsBase AssumeDefaultVersionWhenUnspecified ServiceInfoControllerBase SubstituteApiVersionInUrl IApiDescriptionProvider AddCommonApiVersioning Asp.Versioning.OpenApi HeaderApiVersionReader ServiceInfoController ServiceInfoV2Response"},{"u":"/docs/adr/046-http-api-versioning.html#rationale","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Rationale","x":"- Header selection keeps URLs stable. Routing stays version-free, so gateway route maps, client URL builders, and OpenAPI paths do not fork per version; a caller opts into a…","i":"AssumeDefaultVersionWhenUnspecified ServiceInfoControllerBase AddCommonApiVersioning ReportApiVersions ServiceInfo"},{"u":"/docs/adr/046-http-api-versioning.html#trade-offs","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Trade-offs","x":"- The class-level version attributes are not inherited. Each per-service subclass must repeat the [ApiVersion(...)] and routing attributes (the same inheritance caveat ADR-036…","i":"AddCommonApiVersioning MapCommonOpenApi OAuthController ApiVersion"},{"u":"/docs/adr/046-http-api-versioning.html#related","d":"ADR-046: HTTP API Versioning Strategy","k":"Architecture Decision Records","t":"Related","x":"ADR-010 (integration-event schema versioning: the asynchronous, SchemaVersion-carried, consumer-resolved axis this deliberately contrasts with; HTTP versioning here is…","i":"ServiceInfoVersioningContractTestsBase OAuthController ApiController SchemaVersion ApiVersion controller Route"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#status","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-08-07 (validator hoisted into a shared generic, the 30-second constant moved, the two apps revoke at different speeds). Revised 2026-08-23:…","i":"MiddlewarePipelineBuilder WebApplicationExtensions"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#context","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Context","x":"Soft-delete is the framework's default deletion model (ADR-005): AuditableBaseEntity.Delete() sets IsDeleted = true and EF global query filters hide the row, but the record…","i":"AuditableBaseEntity.Delete HttpContext.User IsDeleted true"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#decision","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Decision","x":"Add a shared-pipeline middleware, SoftDeletedUserMiddleware (Source/Presentation/MMCA.Common.API/Middleware/SoftDeletedUserMiddleware.cs:31, BR-133), that rejects an…","i":"DeleteUserHandler.OnAfterSoftDeleteAsync MiddlewarePipelineBuilder.CreateDefault SoftDeletedUserCache.MarkDeletedAsync SoftDeletedUserCache.MarkerDuration context.RequestServices.GetService MiddlewarePipelineBuilder.Build SoftDeletedUserMiddlewareTests AuditableAggregateRootEntity SoftDeletedUserCache.KeyFor UseCommonMiddlewarePipeline ICurrentUserService.UserId TenantResolutionMiddleware"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#rationale","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Rationale","x":"- Bounds the stateless-JWT revocation gap cheaply. Stateless JWT (ADR-004) has no built-in revocation, so a deactivated account would otherwise stay usable for the full remaining…","i":"ISoftDeletedUserValidator SoftDeletedUserValidator MMCA.Common.API TUser User"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#trade-offs","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Trade-offs","x":"- Revocation is bounded, not immediate. A soft-deleted user whose status is cached as not-deleted keeps passing until that cache entry expires (up to 30 seconds), unless the…","i":"SoftDeletedUserCache.MarkDeletedAsync SoftDeletedUserCache.MarkerDuration ISoftDeletedUserValidator DeleteUserHandler"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#related","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (soft-delete is the deletion model whose still-authenticated tokens this middleware revokes; deleting a user is a soft-delete, not a row removal), ADR-004 (the stateless…","i":"TenantResolutionMiddleware"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#revision-2026-08-07","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"Re-verified against current source. The decision is unchanged, but three things it described have moved: the validator implementation, the home of the 30-second constant, and the…","i":"SoftDeletedUserCache.MarkerDuration SoftDeletedUserMiddleware SoftDeletedUserValidator TimeSpan.FromSeconds DeleteUserHandler CacheDuration UserId TUser true User"},{"u":"/docs/adr/047-soft-deleted-user-session-revocation.html#revision-2026-08-23","d":"ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"Re-verified against current source. The decision, the cache design, the fail-open policy and the per-app asymmetry are all unchanged; what moved is where the middleware's…","i":"MiddlewarePipelineBuilder.CreateDefault app.UseCommonMiddlewarePipeline MiddlewarePipelineBuilder.Build UseCommonMiddlewarePipeline WebApplicationExtensions.cs TenantResolutionMiddleware ISoftDeletedUserValidator SoftDeletedUserMiddleware SoftDeletedUserFilter UseAuthentication TenantResolution UseAuthorization"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#status","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-15). Revised 2026-07-21 (corrected the empty-placeholder-folder inventory and the Directory.Build.props and ADC User source citations). Revised 2026-07-28…","i":"ActivityIdentifierType Directory.Build.props SponsorIdentifierType UserIdentifierType StronglyTypedIds User"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#context","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Context","x":"Every entity needs an identity type. The framework's base entity is generic over that type: BaseEntity constrains it to notnull and exposes a single required init Id…","i":"UserIdentifierType TIdentifierType IBaseEntity BaseEntity readonly required notnull record struct UserId Value Guid"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#decision","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Decision","x":"Model every identifier as a primitive named through a global-using alias, declared per module, not as a wrapper struct. - Identity is a primitive behind an alias. Each module…","i":"EntityTypeConfigurationSQLServer AuditableAggregateRootEntity AuthenticationServiceBase Directory.Build.props SpeakerIdentifierType AuditableBaseEntity UserIdentifierType LinkedSpeakerId IdentifierType LastModifiedBy GetRepository System.Guid"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#rationale","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Rationale","x":"- Readable signatures at zero runtime cost. GetRepository () reads as intent while the CLR sees a plain int. There is no allocation, boxing, or wrapper indirection per…","i":"UserIdentifierType IEntityDTOMapper System.Text.Json GetRepository JsonConverter IBaseEntity BaseEntity IBaseDTO Shared Guid User int"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#trade-offs","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Trade-offs","x":"- No compile-time protection against swapping same-typed identifiers. An alias is a type synonym, not a distinct type. Because most aliases resolve to int, the compiler will not…","i":"SessionIdentifierType SpeakerIdentifierType UserIdentifierType Shared Guid int"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#related","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Related","x":"ADR-001 (the per-entity DTO mappers are parameterized by this identifier type, IEntityDTOMapper ), ADR-034 (the generic entity controllers and query contract ride on the same…","i":"IEntityDTOMapper TIdentifierType TEntityDTO TEntity Shared"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#revision-2026-08-18","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"No decision, no behavior and no citation in this record changed. What changed is the standing of the deferral it records. The last Trade-offs entry above (\"Revisiting the trade…","i":"UserIdentifierType TIdentifierType CheckIn Source razor int"},{"u":"/docs/adr/048-primitive-identifier-type-aliases.html#revision-2026-08-23","d":"ADR-048: Primitive Identifier Type Aliases over Strongly-Typed ID Structs","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"No decision and no rationale changed. Two counts did, both because Conference gained an alias. Conference's alias file declares seventeen aliases, ActivityIdentifierType = int…","i":"ActivityIdentifierType SpeakerIdentifierType System.Guid Guid int"},{"u":"/docs/adr/049-library-configureawait-policy.html","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records"},{"u":"/docs/adr/049-library-configureawait-policy.html#status","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-20; measurements re-anchored 2026-08-07, 2026-08-14, 2026-08-18 and 2026-08-23)."},{"u":"/docs/adr/049-library-configureawait-policy.html#context","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common ships as NuGet packages consumed by host applications, not as an application itself. Library code that awaits without ConfigureAwait(false) captures the caller's…","i":"SynchronizationContext MMCA.Common.UI.Maui ConfigureAwait editorconfig VSTHRD111 RCS1090 CA2007 MA0004 false"},{"u":"/docs/adr/049-library-configureawait-policy.html#decision","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Decision","x":"Packaged non-UI framework code awaits with ConfigureAwait(false); UI component packages and application code do not. - Enforcement is a build gate, not a convention. The…","i":"TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI editorconfig VSTHRD111 RCS1090 warning CA2007 MA0004 false"},{"u":"/docs/adr/049-library-configureawait-policy.html#rationale","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Rationale","x":"- Correctness for the one consumer that already has a context. The MAUI head consumes Infrastructure/Application/API packages through DI; a sync-over-async call anywhere in that…","i":"ConfigureAwait GetAwaiter GetResult script batch false fixes place step but"},{"u":"/docs/adr/049-library-configureawait-policy.html#trade-offs","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Visual noise in framework source. Every await in Source/ (except UI packages) carries .ConfigureAwait(false) (324 sites at adoption; 767 gated sites as of the 2026-08-23…","i":"ConfigureAwait editorconfig false"},{"u":"/docs/adr/049-library-configureawait-policy.html#related","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Related","x":"ADR-042 (the MAUI package whose synchronization context motivates the policy), ADR-027 (the same \"machine-boundary hygiene as a build gate\" posture applied to culture-explicit…"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-07","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"An audit against the code. The policy did not change; three statements about it did. 1. The exemption covers three packages, not the two the Decision named. The glob is…","i":"CodeAnalysisTreatWarningsAsErrors ServerTokenStorageService TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers PackageId foreach warning CA2007"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-14","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-14)","x":"A re-measurement only. The policy, the gate and the exemption are unchanged; the counts the document quotes were a week old and had moved by roughly 9%. 1. Framework site counts,…","i":"CodeAnalysisTreatWarningsAsErrors ServerTokenStorageService TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers PackageId warning CA2007 dotnet"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-18","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"A re-measurement only, in the same terms as the 2026-08-14 pass. The policy, the gate and the exemption are unchanged; two of the three counted figures moved. 1. Framework site…","i":"CodeAnalysisTreatWarningsAsErrors TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers warning CA2007 dotnet format await"},{"u":"/docs/adr/049-library-configureawait-policy.html#revision-2026-08-23","d":"ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007)","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"A re-measurement only, in the same terms as the 2026-08-18 pass. The policy, the gate and the exemption are unchanged; both counted figures moved. 1. Framework site counts,…","i":"CodeAnalysisTreatWarningsAsErrors ServerTokenStorageService TreatWarningsAsErrors MMCA.Common.UI.Maui MMCA.Common.UI.Web ConfigureAwait MMCA.Common.UI analyzers PackageId warning CA2007 dotnet"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#status","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-21)."},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#context","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Context","x":"Identity issues two credentials on every successful sign-in: a short-lived, stateless JWT access token that every service validates by signature and expiry (ADR-004), and a…","i":"AuthenticationServiceBase TUser"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#decision","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Decision","x":"Mint a stateless JWT access token plus a single, server-stored refresh token that rotates on every use, with a token mismatch triggering revocation. - Access token is stateless;…","i":"TokenService.GetPrincipalFromExpiredToken JwtSettings.AccessTokenExpirationMinutes JwtSettings.RefreshTokenExpirationDays TokenService.GenerateRefreshToken TokenService.RefreshTokenLifetime TokenService.GenerateAccessToken RandomNumberGenerator.GetBytes user.RevokeRefreshToken user.UpdateRefreshToken AuthenticationService RefreshTokenLifetime RefreshTokenExpiry"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#rationale","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Rationale","x":"- Short access token plus refresh keeps the hot path stateless. Every service validates the access token with no store lookup (ADR-004); the short exp bounds the revocation gap,…","i":"exp"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#trade-offs","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Trade-offs","x":"- One refresh token per user means one live session. A new login overwrites the single stored token (AuthenticationServiceBase.cs:298), so signing in on a second device…","i":"JwtSettings.RefreshTokenExpirationDays RefreshTokenExpirationDays RefreshTokenLifetime TimeSpan.Zero TokenService"},{"u":"/docs/adr/050-jwt-refresh-token-rotation.html#related","d":"ADR-050: JWT Access Tokens with a Single Rotating Refresh Token and Reuse Detection","k":"Architecture Decision Records","t":"Related","x":"ADR-004 (the stateless RS256/JWKS access token this refresh flow reissues, and the algorithm pinning GetPrincipalFromExpiredToken relies on), ADR-032 (the password hashing that…","i":"GetPrincipalFromExpiredToken AuthenticationServiceBase TUser"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#status","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-23). Revised 2026-08-14 (SetTokensAsync now writes the refresh token and the access token under one shared guard, so a failed refresh-token write also drops…","i":"SetTokensAsync"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#context","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Context","x":"ADR-022 and ADR-050 describe the two server halves of authentication: the Blazor host's HttpOnly session cookie that survives SSR prerender (ADR-022), and the Identity service's…","i":"HttpContext"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#decision","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Decision","x":"Model the client token lifecycle as two small abstractions (ITokenStorageService for persistence, ITokenRefresher for reacquisition) plus a shared bearer-attaching handler and a…","i":"AddClientAuthSessionCookieSync JwtAuthenticationStateProvider SameOriginProxyTokenRefresher ISessionCookieSync.SyncAsync AddCommonServerTokenStorage AddCommonMauiTokenStorage ServerTokenStorageService mmcaAuthSession.getToken NotifyUserAuthentication AcquireAccessTokenAsync DirectApiTokenRefresher WasmTokenStorageService"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#rationale","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Rationale","x":"- One application surface, three storage stories. Pages, services, and the HTTP pipeline talk to ITokenStorageService and AuthenticationStateProvider only; the head-specific…","i":"AuthenticationStateProvider ITokenStorageService"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#trade-offs","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Trade-offs","x":"- The browser heads depend on the same-origin UI host. SameOriginProxyTokenRefresher only works where the UI host serves the /auth/session/ endpoints; a browser head deployed…","i":"JwtAuthenticationStateProvider SameOriginProxyTokenRefresher MMCA.Common.UI.Maui MMCA.Common.UI.Web MMCA.Common.slnx MMCA.Common.UI AuthorizeView SecureStorage"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#related","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Related","x":"ADR-022 (the Blazor host's HttpOnly session cookie and the /auth/session/ endpoints the browser refresher proxies through), ADR-050 (the single rotating refresh token with reuse…","i":"DirectApiTokenRefresher MauiTokenStorageService"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#revision-2026-08-07","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Revision (2026-08-07)","x":"The MAUI half of ITokenStorageService is no longer app-local. The original Decision left the SecureStorage-backed implementation in each app because it depends on the MAUI…","i":"JwtAuthenticationStateProvider MauiTokenStorageService.cs AddCommonMauiTokenStorage DirectApiTokenRefresher MauiTokenStorageService SecureStorage.Default ITokenStorageService MMCA.Common.UI.Maui auth_refresh_token auth_access_token ClearTokensAsync MMCA.Common.slnx"},{"u":"/docs/adr/051-client-auth-token-lifecycle.html#revision-2026-08-14","d":"ADR-051: Client-Side Authentication Token Lifecycle Across Render Modes","k":"Architecture Decision Records","t":"Revision (2026-08-14)","x":"SetTokensAsync closed a gap the original hoist left open. Point 3 above previously described the method as writing the refresh token first and dropping both tokens only when the…","i":"SetTokensAsync catch try"},{"u":"/docs/adr/052-background-job-execution.html","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records"},{"u":"/docs/adr/052-background-job-execution.html#status","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-24). Revised 2026-08-23 (post-commit enqueue is recorded as two patterns, not one: see the revision at the end)."},{"u":"/docs/adr/052-background-job-execution.html#context","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Context","x":"Some requests trigger work that cannot run inside the request: an AI scoring pass over an event's sessions takes minutes and issues one paid API call per session, and a…","i":"RunScoringInBackgroundAsync IHostApplicationLifetime eventId"},{"u":"/docs/adr/052-background-job-execution.html#decision","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Decision","x":"In-process background work runs as a bounded queue plus a single-reader hosted drain. Nothing starts an untracked Task from a request. - A bounded Channel per job kind,…","i":"BoundedChannelFullMode.DropOldest LiveChannelPublishProcessor unitOfWork.SaveChangesAsync LiveChannelPublishQueue SessionScoringProcessor sp.GetRequiredService IServiceScopeFactory SessionScoringQueue BackgroundService SaveChangesAsync TryAddSingleton ITransactional"},{"u":"/docs/adr/052-background-job-execution.html#rationale","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Rationale","x":"- The host lifetime is the point. A BackgroundService is the only in-process shape the host can cancel and await. Everything else in the decision follows from wanting that. - The…","i":"BackgroundService TryEnqueue"},{"u":"/docs/adr/052-background-job-execution.html#trade-offs","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Trade-offs","x":"- In-process only. The queue does not survive a restart and does not span replicas. Accepted because every current job is either ephemeral (a lost broadcast is a missed UI…","i":"DropOldest Wait"},{"u":"/docs/adr/052-background-job-execution.html#related","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox, for work that must be durable and cross-process, and the post-commit deferral this relies on), ADR-008 (the broker, for cross-service work), ADR-014 (the…"},{"u":"/docs/adr/052-background-job-execution.html#revision-2026-08-23","d":"ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain)","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"The decision is unchanged: post-commit work is still enqueued only once the write is durable. What changed is the record of how. This ADR stated the domain-event handler as the…","i":"SessionQuestionUpvoteChangedHandler TransactionalCommandDecorator SessionQuestionUpvoteChanged unitOfWork.SaveChangesAsync LivePollVoteChangedHandler BestEffort.ExecuteAsync ModerateQuestionHandler EnqueueModeratedAsync EnqueueSubmittedAsync SubmitQuestionHandler CloseLivePollHandler IDomainEventHandler"},{"u":"/docs/adr/053-dual-registry-package-publishing.html","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#status","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-25). Amended (2026-07-25) to put the pre-decision Context statements in the past tense, to record the MMCA. ID prefix reservation as then-pending, to scope the…","i":"Directory.Build.props MMCA"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#context","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Context","x":"The fifteen MMCA.Common. packages have shipped to GitHub Packages since the first release. That was the right default while the framework had exactly one consumer group (this…","i":"MMCA.Common.API nuget.config local.props MMCA.Common totalHits package dotnet add"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#decision","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Decision","x":"Every release publishes to both registries, from the same tag, in the same workflow run. - release.yml keeps its existing dotnet nuget push to…","i":"github.repository_owner Directory.Build.props PackageProjectUrl PackageReadmeFile Description MMCA.Common PackageIcon PackageTags permissions release.yml README.md ivanball"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#rationale","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Rationale","x":"- The install line has to be true. Documentation that cannot be followed is worse than no documentation, because the reader concludes the project is broken rather than that the…","i":"MMCA"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#trade-offs","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A published version can never be withdrawn. nuget.org allows unlisting, not deletion. A bad release is now permanent public history, which raises the stakes on the release…","i":"release.yml ivanball"},{"u":"/docs/adr/053-dual-registry-package-publishing.html#related","d":"ADR-053: Dual-Registry Package Publishing (nuget.org plus GitHub Packages)","k":"Architecture Decision Records","t":"Related","x":"ADR-016 (lockstep versioning: every package ships at one version, so both registries receive the same fifteen ids per release), ADR-038 (supply-chain provenance: the SBOM hard…"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#status","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-25). Amended (2026-07-28): Store's reconciliation sweep now derives from PeriodicBackgroundService, so the shared-loop and adoption paragraphs are rewritten and…","i":"PeriodicBackgroundService SafeDomainEventHandler TDomainEvent IUnitOfWork maxReplicas"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#context","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Context","x":"Checkout spans a boundary no transaction covers. CheckOutHandler commits the order insert, the cart transition and the atomic conditional stock decrements in one local…","i":"PaymentInitiated CheckOutHandler"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#decision","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Decision","x":"Multi-step workflows are choreographed sagas: each step raises a domain event, and the follow-up or compensating action lives in its own handler. A periodic reconciliation sweep…","i":"OrderPaymentFailedSagaHandler DbUpdateConcurrencyException PaymentReconciliationService OperationCanceledException OrderCancelledSagaHandler PeriodicBackgroundService Order.InventoryRestored SafeDomainEventHandler MarkInventoryRestored IServiceScopeFactory IDomainEventHandler MarkAsPaymentFailed"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#rationale","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Rationale","x":"- No two-phase commit is available, and none is wanted. Transactions are per data source and best-effort sequential (ADR-006), and an external payment provider cannot enlist in a…","i":"Order.InventoryRestored Order.Status SaveChanges Result catch"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#trade-offs","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Trade-offs","x":"- Inconsistency is bounded, not eliminated. Between the cancellation commit and the compensation commit, stock is held against a cancelled order. Between a dropped webhook and…","i":"PaymentInitiated RestoreInventory InventoryItem maxReplicas"},{"u":"/docs/adr/054-saga-compensation-and-reconciliation.html#related","d":"ADR-054: Choreographed Saga Compensation with a Reconciliation Backstop","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox delivery and retry this leans on for compensation redelivery; this record says what the redelivered handler must do), ADR-006 (which accepts \"no…","i":"RowVersion Result"},{"u":"/docs/adr/055-repository-and-specification-contract.html","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/055-repository-and-specification-contract.html#status","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-25). Revised 2026-08-01 (qualified the \"referenced nowhere\" claim about IEntityReader / IEntityQuerier: an ADC doc comment now names IEntityQuerier, though no…","i":"DependencyInjection.cs DependencyInjection EFReadRepository.cs IEntityQueryService QuerySpecification SessionsController Expression.Invoke EFReadRepository IEntityQuerier IRepository.cs IEntityReader ListAsync"},{"u":"/docs/adr/055-repository-and-specification-contract.html#context","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Context","x":"Every read an application handler performs has to come from somewhere, and the shape of that contract decides whether the module can still be lifted into its own service later…","i":"TIdentifierType IQueryable DbSet"},{"u":"/docs/adr/055-repository-and-specification-contract.html#decision","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Decision","x":"Data access is repository plus specification: interface-segregated read interfaces for the operations, expression-tree specifications for the predicates, and a build-failing…","i":"CrossSourceSpecification.BuildAsync IUnitOfWork.GetReadRepository OrdersByCustomerSpecification PublishedEventSpecification TableNoTrackingSingleQuery ParameterReplacer.Replace TableNoTrackingSplitQuery OwnedByUserSpecification publicSpecification.And EntityQueryService.cs ProductVariantService SpecificationComposer"},{"u":"/docs/adr/055-repository-and-specification-contract.html#rationale","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Rationale","x":"- A narrow interface is the enforcement, not a style preference. A handler that asks for IEntityReader cannot reach TableNoTracking, because the member is not on the interface.…","i":"GetProjectedAsync TableNoTracking IEntityReader IsSatisfiedBy AllowedFiles GetByIdAsync CountAsync IQueryable Criteria"},{"u":"/docs/adr/055-repository-and-specification-contract.html#trade-offs","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- The ISP split is guidance, not yet a wired dependency. (Superseded by the Revision (2026-08-21) below: the split has real dependents shipped in both MMCA.ADC and MMCA.Store,…","i":"QuerySpecification Expression.Invoke ParameterReplacer Specification.cs ISpecification IEntityReader IUnitOfWork Criteria"},{"u":"/docs/adr/055-repository-and-specification-contract.html#related","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-007 and ADR-008 (the extraction promise the queryable ban exists to protect), ADR-015 (the fitness-function machinery that runs this rule and its per-repo maps), ADR-014 (the…","i":"SpecificationsDoNotNavigateToOtherEntities TIdentifierType"},{"u":"/docs/adr/055-repository-and-specification-contract.html#revision-2026-08-18","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Revision (2026-08-18)","x":"Five changes, four of them widening the contract and one of them fixing a correctness defect. The decision this record states is unchanged: data access is still repository plus…","i":"NavigationMetadata.UnsupportedIncludes QueryFieldService.ApplySorting PushNotificationDTOProjection PushNotificationDTOProjector KeysetQueryBuilder.Compare PaginationTieBreakProperty EFReadRepositoryDecorator CrossSourceSpecification Error.InvalidEntityField SpecificationExtensions KeysetCollectionResult ExecuteProjectedAsync"},{"u":"/docs/adr/055-repository-and-specification-contract.html#revision-2026-08-21","d":"ADR-055: Repository plus Specification as the Data-Access Contract","k":"Architecture Decision Records","t":"Revision (2026-08-21)","x":"Nothing in the contract changed; its consumers did. This revision records the first real adoption of the two surfaces this record had honestly flagged as unconsumed: the narrow…","i":"PublicConferenceVisibility SpecificationExtensions specification.Criteria ProductVariantService GetPageByCursorAsync GetProjectedAsync GetReadRepository AndSpecification IReadRepository IEntityQuerier IEntityReader specification"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#status","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28). Revised 2026-08-14: re-anchored the host, base-class and AppHost citations to their current lines; scoped the \"only @rendermode attributes\" enumeration to…","i":"InteractiveServer rendermode App.razor MudTable ADCHome"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#context","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Context","x":"Both web applications are Blazor Web Apps: a static server-rendered (SSR) prerender pass produces the first HTML, then an interactive runtime takes over, either a Blazor Server…","i":"InteractiveAuto App.razor Authorize"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#decision","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Decision","x":"Run one render mode for the entire routable component tree, chosen at the application root, default InteractiveAuto, with prerendering left on and the resulting double fetch…","i":"AddInteractiveWebAssemblyComponents AddInteractiveWebAssemblyRenderMode AddInteractiveServerComponents AddInteractiveServerRenderMode RendererInfo.IsInteractive RenderMode.InteractiveAuto PersistentComponentState PrerenderFetchTimeoutMs InteractiveWebAssembly DataGridListPageBase OnParametersSetAsync RegisterOnPersisting"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#rationale","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Rationale","x":"- InteractiveAuto gets both halves without asking page authors to choose. The first visit gets the Server circuit's immediate interactivity while the WASM bundle downloads in the…","i":"InteractiveServer InteractiveAuto CatalogBrowse Authorize"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#trade-offs","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Trade-offs","x":"- Everything shared has to run in both runtimes. The WASM-compatibility layer rule (MMCA.Common.LayerEnforcement.targets:75-88) forbids the shared UI package from touching…","i":"RendererInfo.IsInteractive AddAdditionalAssemblies DataGridListPageBase MMCA.Common.UI.Web OnAfterRenderAsync InteractiveServer InteractiveAuto CatalogBrowse AddUIShared Program.cs Routes"},{"u":"/docs/adr/056-blazor-render-mode-strategy.html#related","d":"ADR-056: Blazor Render-Mode Strategy for the Web Heads","k":"Architecture Decision Records","t":"Related","x":"ADR-022 (reads the HttpOnly session cookie during the SSR prerender pass this decision keeps enabled), ADR-027 (flows one culture through the SSR to Server to WASM sequence this…","i":"InteractiveAuto"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#status","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28). Revised 2026-08-01: the diff now fails closed in both repos (the true is gone and MMCA.Store's build-and-test checkout sets fetch-depth: 0), so the…","i":"true"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#context","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Context","x":"ADR-030 decides who applies a migration: every service host runs DatabaseInitStrategy = Migrate and self-applies its pending EF Core migrations at startup as the sole migrator,…","i":"DatabaseInitStrategy containerapp DropColumn migrations adee5058 revision Migrate dotnet sqlcmd copy"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#decision","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Decision","x":"Schema changes follow expand/contract, and a CI step enforces the contract half. - Expand now, contract later, as a written rule. Adding nullable columns, new tables and new…","i":"OutboxMessages InboxMessages pull_request CreateIndex DropColumn Migrations DropIndex DropTable IsDeleted base_ref release added"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#rationale","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Rationale","x":"- Rollback is one-way for schema, so the check belongs where the drop is still cheap. The only moment a destructive migration can be reconsidered for free is the PR that adds it;…","i":"Down"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#trade-offs","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- Three operations, not a model of compatibility. AlterColumn narrowing a type or flipping a column to NOT NULL, DropForeignKey, DropPrimaryKey, DropSchema, RenameColumn and a…","i":"migrationBuilder.Sql DropForeignKey DropPrimaryKey RenameColumn AlterColumn DropSchema diff main true with git"},{"u":"/docs/adr/057-expand-contract-schema-evolution-gate.html#related","d":"ADR-057: Expand/Contract Schema Evolution Enforced as a CI Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-030 (decides that each service self-applies its migrations at startup, which is precisely why a rolled-back revision meets the new schema; this ADR constrains what those…"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#status","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28; revised 2026-08-14, 2026-08-18, and 2026-08-23)."},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#context","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Context","x":"ADR-015 turned the architecture invariants into build-gating tests, and drew its own boundary explicitly: the fitness suite asserts \"structure / registration, not runtime…"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#decision","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Decision","x":"Ship the runtime conformance suites in the MMCA.Common.Testing package as abstract behavioral bases that each consuming host subclasses, and run every one of them against a host…","i":"ServiceInfoVersioningContractTestsBase SqlServerIntegrationTestFixtureBase MiddlewarePipelineOrderTestsBase MMCA.Common.Testing.Architecture ProductionHostApplicationFactory DecoratorPipelineOrderTestsBase MiddlewarePipelineOrderTests.cs ProblemDetailsContractTestsBase AssertProblemDetailsShapeAsync GracefulShutdownTestsBase AddApplicationDecorators ChangePreferencesCommand"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#rationale","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Rationale","x":"- Runtime conformance is the half ADR-015 excluded. Structural rules answer \"is the code shaped correctly\"; these suites answer \"does the composed host behave correctly\". A host…","i":"Development Production"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#trade-offs","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Trade-offs","x":"- Opt-in per host, exactly like ADR-015. The framework ships the suites; a host gets the gate only once someone writes the subclass. That is the same audit-the-inventory caveat,…","i":"CorePublicResources MinimumPathCount status title"},{"u":"/docs/adr/058-runtime-conformance-suites-as-a-package.html#related","d":"ADR-058: Runtime Conformance Suites Shipped as a Package","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (the structural / registration fitness layer this complements; its stated non-goal, \"not runtime behavior\", is exactly this ADR's scope, and the two tiers ship as two…","i":"DecoratorPipelineOrderTestsBase ProblemDetailsContractTestsBase"},{"u":"/docs/adr/059-module-contract-and-composition.html","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records"},{"u":"/docs/adr/059-module-contract-and-composition.html#status","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28; revised 2026-08-14)."},{"u":"/docs/adr/059-module-contract-and-composition.html#context","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Context","x":"The framework's headline claim is that an application is built as a modular monolith and later extracted into services without rewriting business logic. ADR-008 states the…","i":"ModuleLoader"},{"u":"/docs/adr/059-module-contract-and-composition.html#decision","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Decision","x":"Make IModule the single composition contract, discover implementations by reflection, register them in topological dependency order, and represent a disabled module by stub…","i":"DisabledSessionBookmarkValidationService AppDomain.CurrentDomain.GetAssemblies DisabledEventLiveValidationService ModuleControllerFeatureProvider DisabledUserSalesExportService DisabledProductVariantService SalesUserDataExportSection ValidateRemoteDependencies InvalidOperationException Activator.CreateInstance AddUserDataExportSection DisabledCustomerService"},{"u":"/docs/adr/059-module-contract-and-composition.html#rationale","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Rationale","x":"- Reflection discovery keeps hosts out of the module registry business. A host calls one method and gets whatever modules its assembly graph contains; adding a module is a…","i":"RequiresDependencies RemoteDependencies appsettings.json Dependencies Modules true"},{"u":"/docs/adr/059-module-contract-and-composition.html#trade-offs","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Trade-offs","x":"- The AppDomain scan is the fragile default and the one everybody uses. The loader's own documentation warns that the AppDomain scan sees only assemblies already loaded, so a…","i":"ModuleConformanceTestsBase ValidateRemoteDependencies Activator.CreateInstance IBookmarkCountService RegisterDisabledStubs RequiresDependencies AddTypedGrpcClient Dependencies ModuleName Complete Register Enabled"},{"u":"/docs/adr/059-module-contract-and-composition.html#related","d":"ADR-059: The IModule Contract and Reflection-Based Module Composition","k":"Architecture Decision Records","t":"Related","x":"ADR-008 (the extraction topology that consumes this model: \"a service is the monolith with one module enabled\" is a statement about ModuleLoader plus the Disabled stubs, cited…","i":"AddApplicationDecorators ModuleLoader"},{"u":"/docs/adr/060-performance-regression-gate.html","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records"},{"u":"/docs/adr/060-performance-regression-gate.html#status","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-07-28). Revised 2026-08-01 (corrected the count in Trade-offs: the single ratio floor names two of the eight benchmarks, so six, not seven, are gated on…","i":"ci.yml"},{"u":"/docs/adr/060-performance-regression-gate.html#context","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Context","x":"Rubric section 12 asks for hot-path efficiency that is measured, not assumed (Website/docs-src/governance/ArchitectureEvaluationCriteria.md:355). MMCA.Common has a…","i":"IsSatisfiedBy"},{"u":"/docs/adr/060-performance-regression-gate.html#decision","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Decision","x":"Measure the hot-path suite on every code PR and verify the results against a committed baseline that carries two rule kinds: absolute allocation ceilings where the measurement is…","i":"ApplyFilters_ThreeMixedOperators IsSatisfiedBy_RecompileEachCall IsSatisfiedBy_CachedCompile allocationCeilingsBytes MMCA.Common.slnx PackageReference System.Text.Json BenchmarkDotNet MemoryDiagnoser fastBenchmark slowBenchmark Performance"},{"u":"/docs/adr/060-performance-regression-gate.html#rationale","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Rationale","x":"- A ratio is a property of the code; an absolute nanosecond count is a property of the runner. Both benchmarks in a floor run in the same process, on the same machine, in the…","i":"MemoryDiagnoser Specification TEntity TId"},{"u":"/docs/adr/060-performance-regression-gate.html#trade-offs","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The Short job cannot see small latency regressions. Three warmup and three iterations (ci.yml:360) give wide confidence intervals: enough for a 1000x floor and for counting…","i":"ApplyFilters release.yml changes main push"},{"u":"/docs/adr/060-performance-regression-gate.html#related","d":"ADR-060: Performance-Regression Gate (Committed Benchmark Baseline in CI)","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (structural fitness functions, which explicitly stop at structure and registration; this is their runtime-cost counterpart), ADR-038 (the other build-gating control set,…"},{"u":"/docs/adr/061-runtime-secret-management.html","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records"},{"u":"/docs/adr/061-runtime-secret-management.html#status","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01; vault-backed configuration source recorded and citations re-anchored 2026-08-23)."},{"u":"/docs/adr/061-runtime-secret-management.html#context","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Context","x":"A Container App can hold a credential two ways: as a literal value in the app's own secrets collection, or as a reference to a Key Vault secret that the platform resolves at…","i":"DefaultAzureCredential secrets"},{"u":"/docs/adr/061-runtime-secret-management.html#decision","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Decision","x":"Every production secret lives in Azure Key Vault and reaches the app as a keyVaultUrl secret reference resolved by a user-assigned managed identity; the same identity also lets a…","i":"AddCommonKeyVaultConfiguration azureADOnlyAuthentication USE_MANAGED_IDENTITY_SQL DefaultAzureCredential useManagedIdentitySql AZURE_CLIENT_ID hasSmtpPassword IConfiguration MMCA.Templates KeyVault__Uri keyVaultUrl claude.yml"},{"u":"/docs/adr/061-runtime-secret-management.html#rationale","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Rationale","x":"- A reference has one home; a literal has as many homes as it has consumers. Three vault secrets in each repo are referenced by more than one app: Redis and the broker by all…"},{"u":"/docs/adr/061-runtime-secret-management.html#trade-offs","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Trade-offs","x":"- One identity means vault-wide read for every app that carries it. A Key Vault Secrets User grant is scoped to the vault, so any app running as the shared identity can read…","i":"AZURE_CLIENT_ID main.bicep EXTERNAL listKeys PROVIDER secrets CREATE secure unused FROM USER"},{"u":"/docs/adr/061-runtime-secret-management.html#related","d":"ADR-061: Runtime Secret Management via Key Vault References and Managed Identity","k":"Architecture Decision Records","t":"Related","x":"ADR-037 (037-field-level-encryption-at-rest.md:108-110 directs a consumer to keep the field-encryption key in Key Vault but decides no delivery mechanism, and nothing wires that…"},{"u":"/docs/adr/062-slo-alerting-as-code.html","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/062-slo-alerting-as-code.html#status","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01). Revised 2026-08-18: Store's two operational extras (the outbox-dead-letter scheduled query rule and the outside-in Gateway availability web test with its…","i":"OPERATIONS.md main.bicep main"},{"u":"/docs/adr/062-slo-alerting-as-code.html#context","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Context","x":"ADR-041 standardized what the fleet emits: RED histograms off the CQRS pipeline, an outbox dead-letter counter, correlation ids, exporters, and the cost knobs that keep ingestion…"},{"u":"/docs/adr/062-slo-alerting-as-code.html#decision","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Decision","x":"Declare each consumer's SLO alerts as data in its Bicep template, materialize them as Log Analytics scheduled query rules, and make the alert-to-runbook pairing a build gate…","i":"EveryRunbookAlertSection_MapsToAProvisionedAlert SloAlertSpecs_AreDiscovered_GateIsNotVacuous ObservabilityConventionTestsBaseTests ObservabilityConventionTestsBase legacySloMetricAlertSpecs infra.OPERATIONS.md metricMeasureColumn RunbookHeadingRegex alertEmailAddress MinimumAlertSpecs infra.main.bicep ResourceAssembly"},{"u":"/docs/adr/062-slo-alerting-as-code.html#rationale","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Rationale","x":"- Alerts as data, not as portal state. One array is reviewable in a PR, diffable across environments, and re-deployable; the rules, the workbook, and the notification channel are…","i":"enabled false"},{"u":"/docs/adr/062-slo-alerting-as-code.html#trade-offs","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- It is a text gate over IaC, not a check against deployed state. The base matches literal anchors and regexes in the template and headings in markdown. It proves the two files…","i":"environmentName sloAlertSpecs metricAlerts prefix env key"},{"u":"/docs/adr/062-slo-alerting-as-code.html#related","d":"ADR-062: SLO Alerting as Code with an Alert-to-Runbook Build Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-041 (the telemetry this alerts on top of: it defines emission, instrumentation and cost knobs and stops before thresholds, severities and runbooks), ADR-009 (recovery…"},{"u":"/docs/adr/063-accessibility-conformance-gate.html","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#status","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01). Revised 2026-08-14: refreshed the E2ETestBase helper line anchors (explanatory comments were added above ScanGridAsync), the two consumer suite scan counts…","i":"ScanGridAsync E2ETestBase"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#context","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Context","x":"Accessibility was documented before it was enforced. The narrative guide (common-ACCESSIBILITY.md, rubric section 21) named WCAG 2.1 AA as the target for the shared…","i":"MMCA.Common.UI"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#decision","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Decision","x":"Ship WCAG 2.1 AA as a named, versioned test contract in MMCA.Common.Testing.E2E, assert it from the package's own workflow bases, and wire it as a required merge check and a…","i":"AxeOptions.Wcag21AaExceptMudPagerCombobox AssertNoAccessibilityViolationsAsync AccessibilityViolationException PasswordResetTestsBase.cs MMCA.Common.Testing.E2E ProfileManagementTests AxeOptions.Wcag21Aa PrimaryContrastText WarningContrastText GalleryAxeTestBase ErrorContrastText AxeRunOptions"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#rationale","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Rationale","x":"- A named constant is the contract. Putting the rule set in a shipped, referenced symbol rather than in each repo's test setup means \"what WCAG 2.1 AA means here\" has exactly one…","i":"ProfileManagementTestsBase UserRegistrationTestsBase PasswordResetTestsBase UserLoginTestsBase"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#trade-offs","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- Best-practice rules are out of scope, deliberately. Findings axe would classify as best practice (and anything WCAG AAA) are not measured at all, so the gate can be green on a…","i":"Wcag21AaExceptMudPagerCombobox AccessibilityTests ScanGridAsync skipped success deploy"},{"u":"/docs/adr/063-accessibility-conformance-gate.html#related","d":"ADR-063: WCAG 2.1 AA Accessibility as a Shipped Test Contract and CI Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-015 (architecture fitness functions: the structural tier this parallels at the browser tier, and the same invariant-over-discipline posture), ADR-058 (runtime conformance…"},{"u":"/docs/adr/064-deploy-recency-gates.html","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records"},{"u":"/docs/adr/064-deploy-recency-gates.html#status","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-01). Revised 2026-08-07: the MMCA.Helpdesk workflow inventory below was corrected (it also carries release-templates.yml, and its ci.yml runs two jobs, not…","i":"ci.yml"},{"u":"/docs/adr/064-deploy-recency-gates.html#context","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Context","x":"A production rollout in both deployed apps waits on a list of jobs in deploy.needs (MMCA.ADC/.github/workflows/deploy.yml:866, MMCA.Store/.github/workflows/deploy.yml:862). Most…","i":"deploy.needs"},{"u":"/docs/adr/064-deploy-recency-gates.html#decision","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Decision","x":"A production deploy is blocked not only on green tests but on proof of recency for out-of-band verification: three gates assert that a real drill, a real load run and a real…","i":"skip_freshness_gates skip_justification github.event_name workflow_dispatch FRESHNESS_DAYS workflow_runs deploy.needs release.yml foundation updated_at cancelled contents"},{"u":"/docs/adr/064-deploy-recency-gates.html#rationale","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Rationale","x":"- A proof with no expiry date is documentation, not a control. ADR-009 already required the drill to be recorded, and recording it was the honest half of the problem; a record…"},{"u":"/docs/adr/064-deploy-recency-gates.html#trade-offs","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Trade-offs","x":"- An unrelated stale proof blocks an unrelated deploy. A one-line hotfix does not ship when the monthly k6 cron did not fire, and the failure surfaces after merge: the gate job…","i":"deploy"},{"u":"/docs/adr/064-deploy-recency-gates.html#related","d":"ADR-064: Deploy Preconditions as Proof-of-Recency Gates","k":"Architecture Decision Records","t":"Related","x":"ADR-009 (states the recovery objectives and requires that a restore be drilled and recorded; this record decides that a deploy is blocked on how recently that drill, and the…"},{"u":"/docs/adr/065-scaffolding-templates.html","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","x":"Status: Accepted (2026-08-02). Revised 2026-08-07: the staged analyzer delta relaxes three rules rather than one; mmca-module prints seven wire-ups rather than five, and a…","i":"Directory.Packages.props"},{"u":"/docs/adr/065-scaffolding-templates.html#context","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Context","x":"Build by hand is accurate and complete, and phases 1 through 6 of it are transcription work (common-BUILD-BY-HAND.md:96 through :1049). Its own instruction for the load-bearing…","i":"AddApplicationDecorators Directory.Packages.props Directory.Build.targets Directory.Build.props launchSettings.json IArchitectureMap MMCA.Templates editorconfig nuget.config global.json install WaitFor"},{"u":"/docs/adr/065-scaffolding-templates.html#decision","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Decision","x":"Ship a dotnet new template pack, MMCA.Templates, containing four templates: The template content is the MMCA.Helpdesk reference application itself, staged at pack time.…","i":"IntegrationEventContractTestsBase IntegrationEventContractTests SQLServerMigrationsAssembly WithSQLServerDataSource TreatWarningsAsErrors AddErrorResources appsettings.json IArchitectureMap Contoso.Support RequesterUserId MMCA.Templates MMCA.Helpdesk"},{"u":"/docs/adr/065-scaffolding-templates.html#rationale","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Rationale","x":"Deriving from the seed rather than maintaining a template tree is the whole design. A hand-maintained copy of a 12-project solution drifts within one release, and drift in a…","i":"MMCA.Common.Templates MMCA.Templates MMCA.Helpdesk sourceName Helpdesk install Tickets dotnet Ticket new"},{"u":"/docs/adr/065-scaffolding-templates.html#trade-offs","d":"ADR-065: Scaffolding templates derived from the reference app","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two documented one-time fixups in every generated app, above (one of them covering all three relaxed rules). The alternative to the SA1210 half of the delta was moving every…","i":"IntegrationEventContractTestsBase IntegrationEventContractTests MMCA.Common copyOnly dotnet SA1210 using Fact new"},{"u":"/docs/adr/066-broker-transport-selection.html","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records"},{"u":"/docs/adr/066-broker-transport-selection.html#status","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07). Revised 2026-08-14 (source citations re-anchored; the ADC AppHost comment that used to say no WithBroker() was wired has been corrected in code, so the…","i":"WithBroker"},{"u":"/docs/adr/066-broker-transport-selection.html#context","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Context","x":"ADR-003 decides that integration events leave an aggregate through the outbox and are published by OutboxProcessor via IMessageBus, and it settles the dispatch question…","i":"OutboxProcessor IMessageBus"},{"u":"/docs/adr/066-broker-transport-selection.html#decision","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Decision","x":"Keep one IMessageBus abstraction with a three-value transport selector, choose the value at the deployment edge (never in application code), configure both broker transports…","i":"Bus.Factory.CreateUsingAzureServiceBus ResolveBrokerConnectionString MessageBus__ConnectionString ConnectionStrings__rabbitmq RootManageSharedAccessKey ConfigureBrokerTransport EnableDelayedRedelivery RetryMaxIntervalSeconds RetryMinIntervalSeconds builder.Configuration UseDelayedRedelivery UsingAzureServiceBus"},{"u":"/docs/adr/066-broker-transport-selection.html#rationale","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Rationale","x":"- The transport is a deployment fact, so it lives at the deployment edge. The only difference between a laptop and production is two environment variables set by the AppHost or…","i":"Listen Send"},{"u":"/docs/adr/066-broker-transport-selection.html#trade-offs","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two brokers means two behaviors to keep aligned. Configuration parity is enforced by one code path, but the products still differ (Service Bus supports delayed redelivery…","i":"MessageBus__Provider ConfigureEndpoints WithBroker Manage rabbit"},{"u":"/docs/adr/066-broker-transport-selection.html#related","d":"ADR-066: Broker Transport Selection and Dev/Prod Parity","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox that feeds IMessageBus; this ADR picks the transport underneath it), ADR-016 (the MassTransit v8 pin the emulator tier must work within, which is why the…","i":"IMessageBus Host"},{"u":"/docs/adr/067-ui-module-shell-composition.html","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records"},{"u":"/docs/adr/067-ui-module-shell-composition.html#status","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07)."},{"u":"/docs/adr/067-ui-module-shell-composition.html#context","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Context","x":"ADR-059 decided how a module plugs into the server: an IModule implementation is discovered by reflection, registered in topological order, and a host composes an application out…","i":"MMCA.Common.UI IModule Routes App"},{"u":"/docs/adr/067-ui-module-shell-composition.html#decision","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Decision","x":"Ship the application shell in the framework package and let each module plug into it by implementing IUIModule, resolved from DI as IEnumerable . - The contract is four members,…","i":"AdditionalAssemblies AppBarComponentTypes LayoutComponentTypes AuthorizeRouteView MapRazorComponents DynamicComponent UIModules.Select RedirectToLogin DeviceUIModule RequiredClaim TitleResource AddSingleton"},{"u":"/docs/adr/067-ui-module-shell-composition.html#rationale","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Rationale","x":"- One composition model across both tiers. A module already declares its server-side surface through IModule (ADR-059); declaring its UI surface through IUIModule means \"add a…","i":"AppBarComponentTypes LayoutComponentTypes AuthorizeView Components IUIModule IModule NavMenu"},{"u":"/docs/adr/067-ui-module-shell-composition.html#trade-offs","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Trade-offs","x":"- Assembly is required even when it carries no route. A host-only module that contributes only a layout component still has to return an assembly, which then joins…","i":"AddAdditionalAssemblies AdditionalAssemblies AuthorizeRouteView RequiredClaim MauiUIModule RequiredRole Program.cs IUIModule Assembly NavItems NavMenu page"},{"u":"/docs/adr/067-ui-module-shell-composition.html#related","d":"ADR-067: Shared Blazor Application Shell and IUIModule Composition","k":"Architecture Decision Records","t":"Related","x":"ADR-059 (the server-side IModule contract this mirrors in the presentation layer), ADR-056 (the render-mode strategy for the web heads, which decides how these components render…","i":"TitleResource IModule"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#status","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07)."},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#context","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Context","x":"A domain model has two kinds of small type: the identity of a thing, and a value the thing carries. ADR-048 recorded the identity half: identifiers stay primitives named through…","i":"decimal string"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#decision","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Decision","x":"Model a domain value that carries an invariant as an immutable record value object with a Result-returning factory; keep identifiers primitive (ADR-048). - One abstract record…","i":"PhoneNumberInvariants.EnsurePhoneNumberIsValid ArchitectureRules.DomainFactoriesReturnResult AddressInvariants.EnsureAddressLine1IsValid EmailInvariants.EnsureEmailIsValid NullablePhoneNumberValueConverter NullableEmailValueConverter EmailInvariants.MaxLength PhoneNumberValueConverter DataContractSerializer GetEqualityComponents DateTimeRange.Create ProductVariant.Price"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#rationale","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Rationale","x":"- The invariant belongs to the type, not to every caller. A string email can be validated in one handler and not the next; an Email cannot exist unvalidated, because the only…","i":"NullReferenceException Currency.None Money.Zero OwnsMoney record Result string Email Money"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#trade-offs","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Trade-offs","x":"- The pattern is not uniformly applied. Only three of the seven types have a companion Invariants class; the rest inline their checks. Only Money has a shipped owned-type helper,…","i":"InvalidOperationException DateTimeRange Currency.All PhoneNumber DateRange Money.Add operator Address OwnsOne Create Result string"},{"u":"/docs/adr/068-value-objects-as-validated-primitives.html#related","d":"ADR-068: Value Objects as Validated Domain Primitives","k":"Architecture Decision Records","t":"Related","x":"ADR-048 (the deliberate opposite call for identifiers: primitives behind aliases, wrapper structs rejected, because identifiers cross process boundaries constantly and carry no…","i":"Create Result"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#status","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07). Updated 2026-08-14: Store's adoption has landed and is live (its own dedicated storage account, gated on dataProtectionStorageReady), and the ADC call-site…","i":"dataProtectionStorageReady AddServiceDefaults"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#context","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Context","x":"ASP.NET Core's DataProtection default keeps the key ring in memory, per process. That is correct for a single-process host and wrong for a scaled-out one: every replica generates…","i":"DefaultAzureCredential maxReplicas"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#decision","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Decision","x":"Add one opt-in registration call, AddCommonDataProtection, that persists the key ring to a single Azure blob so every replica of a host shares one ring…","i":"Azure.Extensions.AspNetCore.DataProtection.Blobs KeyManagementOptions.XmlRepository System.Security.Cryptography.Xml AddCommonKeyVaultConfiguration DataProtection__BlobStorageUri DataProtection__KeyVaultKeyUri grantDataProtectionStorageRole PersistKeysToAzureBlobStorage ProtectKeysWithAzureKeyVault dataProtectionStorageReady AddCommonDataProtection IDataProtectionProvider"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#rationale","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Rationale","x":"- The key ring is the smallest thing that has to be shared. Sticky sessions would paper over the symptom while making a replica restart a mass sign-out, and a shared cache would…"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#trade-offs","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Trade-offs","x":"- The key ring is not encrypted at rest today. Gate 2 is implemented but configured nowhere, so the ring is protected by the container being private and the account grant being…","i":"AddCommonDataProtection AZURE_CLIENT_ID MMCA.ADC"},{"u":"/docs/adr/069-shared-data-protection-key-ring.html#related","d":"ADR-069: Shared DataProtection Key Ring for Scaled-Out Hosts","k":"Architecture Decision Records","t":"Related","x":"ADR-022 (the browser session cookies whose decryption this makes replica-independent, together with the antiforgery tokens the SSR pages mint), ADR-008 (the multi-host topology…","i":"DefaultAzureCredential"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#status","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-07). Revised 2026-08-14 (source citations re-anchored; the consumer-repo facade claim narrowed to production code, with the controller-test exception recorded).…","i":"IValidateOptions IOptions"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#context","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Context","x":"Every host in the workspace reads a dozen or more configuration sections: connection strings, SMTP, JWT key material, outbox tuning, message-bus provider, module enablement,…","i":"IOptions"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#decision","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Decision","x":"Bind every settings section through a validating chain that runs at startup, and expose a settings type through a read-only interface when it must be read above Infrastructure. -…","i":"CreateCheckoutSessionCommandValidator GatewayRateLimitingSettings ForgotPasswordHandlerBase IConnectionStringSettings IPushNotificationSettings CheckoutRedirectSettings ConnectionStringSettings PushNotificationSettings RecordRoomCheckInHandler AddCommonAuthentication LoginProtectionSettings SecurityHeadersSettings"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#rationale","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Rationale","x":"- A boot failure is cheaper than a first-use failure. A host that will not start is caught by the deployment, by a local dotnet run, or by CI. A host that starts and fails on the…","i":"Microsoft.Extensions.Options ValidateDataAnnotations EntityControllerBase IApplicationSettings ApplicationSettings IValidatableObject RepositoryFactory ValidateOnStart JwtSettings IOptions dotnet init"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#trade-offs","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- Nothing enforces it. There is no architecture fitness test asserting that a new AddOptions call carries ValidateDataAnnotations().ValidateOnStart(). The uniformity above is…","i":"TenancySettingsValidator ValidateDataAnnotations IDataSourceResolver IValidatableObject IValidateOptions IOptionsMonitor TenancySettings ValidateOnStart JwtSettings AddOptions IOptions Value"},{"u":"/docs/adr/070-fail-fast-configuration-contract.html#related","d":"ADR-070: Fail-Fast Configuration Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-025 (startup warm-up and readiness gating: this contract decides what happens before a host reaches that machinery), ADR-031 (feature flags read from configuration, whose…"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#status","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-12). Amended 2026-08-13: the composition-time string trade-off below was resolved in v1.147.0 by a deferred-resolution overload; see the updated trade-off…"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#context","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Context","x":"ADC's badge check-in feature (ADR-072) needs two things that look like one thing: an attendee's device has to show a QR code, and an organizer's device has to read one. They are…","i":"AddDeviceCapabilityDefaults NSCameraUsageDescription MMCA.Common.UI System.Drawing AddUIShared CAMERA"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#decision","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Decision","x":"Split the feature by what it actually depends on: QR display ships as a shared component, barcode scanning ships as an ADR-042 capability whose native half is opt-in per head. -…","i":"AddDeviceCapabilityDefaults DeviceInfo.Current.Platform MauiBarcodeScannerService NullBarcodeScannerService UseMauiDeviceCapabilities Permissions.RequestAsync ZXing.Net.Maui.Controls IBarcodeScannerService QrErrorCorrectionLevel ScanOnMainThreadAsync TaskCompletionSource MMCA.Common.UI.Maui"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#rationale","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Rationale","x":"- Rendering a QR is not a device concern, so making it one would have been ceremony. As a capability it would have needed an interface, a null fallback and a native override for…","i":"UseMauiDeviceCapabilities MMCA.Common.UI PngByteQRCode IsSupported MauiProgram null try"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#trade-offs","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Trade-offs","x":"- The scan page's strings were resolved at composition, not per call (resolved in v1.147.0). As shipped in v1.145.0, cancelText and cameraDescription were captured into the…","i":"UseCommonBarcodeScanner cameraDescription OnParametersSet MMCA.Common.UI IsSupported QrCodeImage cancelText QRCoder string catch false Func"},{"u":"/docs/adr/071-barcode-scanning-and-qr-display.html#related","d":"ADR-071: Device Capability Pattern for Barcode Scanning and QR Display","k":"Architecture Decision Records","t":"Related","x":"ADR-042 (the capability pattern this extends: contract in MMCA.Common.UI, native implementation in MMCA.Common.UI.Maui, override after AddUIShared), ADR-072 (the ADC feature that…","i":"MMCA.Common.UI.Maui MMCA.Common.UI AddUIShared"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#status","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). Amended (2026-08-14): ADC shipped two attendee-self-recorded scan surfaces (sponsor booth visits and room self check-in), a third CheckInScope, a sixth…","i":"CheckInScope"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#context","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Context","x":"ADC wanted two conference-day capabilities that turn out to be one mechanism. Organizers want to know who actually attended which session, which the schedule cannot tell them: a…"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#decision","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Decision","x":"AttendeeBadge (MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Domain/Badges/AttendeeBadge.cs:17-24) is one row per user holding a single Guid Credential, minted on first…","i":"CheckInInvariants.EnsureTargetMatchesScope CheckInSettings.RoomCheckInGraceMinutes EngagementPermissions.CheckInManage CheckInProcessor.FindExistingAsync EngagementFeatures.SponsorVisits EngagementPointsEntryExportItem PointsActivityType.SponsorVisit EngagementFeatures.RoomCheckIn user_engagement_export.proto EngagementCheckInExportItem Engagement.SponsorVisits leaderboard_display_name"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#rationale","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Rationale","x":"- An opaque credential makes the server the only interpreter. A JWT or HMAC badge would verify offline, but the scanning device is online by necessity (it has to write a check-in…","i":"SessionCheckIn EventCheckIn Regenerate"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#trade-offs","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Trade-offs","x":"- The badge credential is a bearer value. Anyone who photographs an attendee's screen can be checked in as that attendee. The mitigations are that a badge scan is organizer-side,…","i":"DuplicateKeyDetection.IsDuplicateKey SetLeaderboardParticipationHandler GetLeaderboardHandler AttendeeCheckedIn Engagement.Points SessionFeedback SessionCheckIn activity_type IFeatureGated PointsAwarder QuestionAsked FeatureGate"},{"u":"/docs/adr/072-qr-badge-check-in-and-points.html#related","d":"ADR-072: QR Badge Check-In and Points Gamification in ADC","k":"Architecture Decision Records","t":"Related","x":"ADR-071 (the framework halves this consumes: the QR component on /my-badge and the scanner capability behind /check-in), ADR-003 (the outbox path AttendeeCheckedIn and the two…","i":"AttendeeCheckedIn EraseDisplayName"},{"u":"/docs/adr/073-multi-tenancy-model.html","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records"},{"u":"/docs/adr/073-multi-tenancy-model.html#status","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). The implementation lands in the MMCA.Common enterprise capability wave release, alongside the scheduler, audit trail, DSAR export, and CSV export work. It…","i":"MiddlewarePipelineOrderTestsBase MiddlewarePipelineStepNames DesignTimeDbContextHelper MiddlewarePipelineBuilder ApplicationDbContext IgnoreQueryFilters EFReadRepository AddMultiTenancy configuration ITenantEntity OnConfiguring GetService"},{"u":"/docs/adr/073-multi-tenancy-model.html#context","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Context","x":"MMCA.Common already partitions data along two axes and neither of them is a tenant. ADR-006 partitions by source name (every entity resolves to a DataSourceKey(Engine, Name),…","i":"ApplySoftDeleteFilters SoftDeleteFilterName modelBuilder.Entity OnModelCreating HasQueryFilter DataSourceKey OnConfiguring TenantId clrType Engine filter Where"},{"u":"/docs/adr/073-multi-tenancy-model.html#decision","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Decision","x":"Ship shared-schema tenancy as a second named query filter, with per-tenant database routing as a configuration override on the same source key, both opt-in and both inert until a…","i":"CrossTenantWriteException.ForUnresolvedTenant MiddlewarePipelineBuilder.CreateDefault IPhysicalDbContextFactory.Create MiddlewarePipelineOrderTestsBase CosmosDbContext.OnModelCreating TenantSaveChangesInterceptor UseCommonMiddlewarePipeline TenantResolutionMiddleware CrossTenantWriteException DesignTimeDbContextHelper ITenantContext.SetTenant CachingCommandDecorator"},{"u":"/docs/adr/073-multi-tenancy-model.html#rationale","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Rationale","x":"- A query filter is the only place the rule cannot be forgotten. Per-handler Where clauses are correct until the tenth handler, and the tenth handler is a data leak rather than a…","i":"IgnoreQueryFilters DataSourceKey ICacheService RequireTenant Where"},{"u":"/docs/adr/073-multi-tenancy-model.html#trade-offs","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Reads are on discipline where writes are on an invariant. A consumer calling EF's own parameterless IgnoreQueryFilters() on a raw Table surface drops the tenant filter along…","i":"ApplicationLayer_DoesNotUseRawQueryableSurfaces DefaultSqlServerDbContextFactory TenantSaveChangesInterceptor IgnoreQueryFilters ICacheService ITenantEntity tenant_id Table"},{"u":"/docs/adr/073-multi-tenancy-model.html#related","d":"ADR-073: Multi-Tenancy (Shared-Schema Query Filter plus DB-per-Tenant Routing)","k":"Architecture Decision Records","t":"Related","x":"ADR-006 (the source-name axis this composes with: an override re-points a DataSourceKey without changing it, and the per-source outbox this record drains once per tenant),…","i":"IgnoreQueryFilters CosmosDbContext TenancySettings DataSourceKey tenantId TenantId string"},{"u":"/docs/adr/074-recurring-job-scheduler.html","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records"},{"u":"/docs/adr/074-recurring-job-scheduler.html#status","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13; revised 2026-08-14, 2026-08-18). The implementation lands in the MMCA.Common \"enterprise capability wave\" release and is opt-in: a host calls…","i":"AddScheduledJobs configuration"},{"u":"/docs/adr/074-recurring-job-scheduler.html#context","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Context","x":"The framework had two kinds of background work and neither of them is a schedule. OutboxProcessor…","i":"PeriodicBackgroundService OutboxProcessor"},{"u":"/docs/adr/074-recurring-job-scheduler.html#decision","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Decision","x":"A persistent job store plus a single-runner claim lease, reusing the exact idiom the outbox proved. The outbox claims a batch with an ExecuteUpdateAsync that sets LockedUntil and…","i":"DesignTimeDbContextOptions.EnableScheduler ScheduledJobOverrideSettings.Cron DesignTimeDbContextHelper PeriodicBackgroundService Directory.Packages.props EnsurePermissionRegistry ValidateDataAnnotations PollingIntervalSeconds SchedulerSettings.Jobs SyncRegistrationsAsync ResolveCronExpression AuditTrailCleanupJob"},{"u":"/docs/adr/074-recurring-job-scheduler.html#rationale","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Rationale","x":"- The lease is already proven under production load. Multi-replica correctness for recurring work is the hard part, and it was solved once for the outbox: an atomic claim update,…","i":"AddScheduledJobs IUnitOfWork LastRunOn NextRunOn DateTime"},{"u":"/docs/adr/074-recurring-job-scheduler.html#trade-offs","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A polling loop is not a real-time scheduler. Worst-case start lag is one polling interval, 30 seconds at the default, so sub-minute precision is not on offer. A job that must…","i":"LeaseSeconds"},{"u":"/docs/adr/074-recurring-job-scheduler.html#related","d":"ADR-074: Recurring Job Scheduler (Persistent Cron Jobs on the Outbox Claim-Lease Pattern)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox whose claim-lease idiom and smart wait this reuses verbatim, and whose at-least-once posture it inherits along with the idempotency obligation on job bodies),…","i":"SchedulerSettings SchedulerMetrics OutboxMetrics"},{"u":"/docs/adr/075-audit-trail.html","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records"},{"u":"/docs/adr/075-audit-trail.html#status","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13; corrected 2026-08-14: the adoption sweep and the ApplicationDbContext line citations). The implementation lands in the MMCA.Common \"enterprise capability…","i":"ApplicationDbContext IAuditedEntity AddAuditTrail configuration"},{"u":"/docs/adr/075-audit-trail.html#context","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Context","x":"The framework already answers \"who touched this row last\". Every AuditableBaseEntity carries CreatedOn/By and LastModifiedOn/By, stamped by AuditSaveChangesInterceptor on the way…","i":"AuditSaveChangesInterceptor AuditableBaseEntity SaveChangesAsync LastModifiedBy"},{"u":"/docs/adr/075-audit-trail.html#decision","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Decision","x":"AuditTrailSaveChangesInterceptor (Infrastructure Persistence/AuditTrail/) joins the interceptors ApplicationDbContext.OnConfiguring already passes to…","i":"ApplicationDbContext.OnModelCreating ApplicationDbContext.OnConfiguring DomainEventSaveChangesInterceptor AuditTrailSaveChangesInterceptor optionsBuilder.AddInterceptors TenantSaveChangesInterceptor AuditSaveChangesInterceptor DesignTimeDbContextHelper PeriodicBackgroundService PiiRedactor.RedactedToken DiscardAbandonedCapture DependencyInjection.cs"},{"u":"/docs/adr/075-audit-trail.html#rationale","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Rationale","x":"- IAuditableEntity is a statement about a business row, and an audit row is not one. The interface means \"this row stamps who created and last modified it and participates in…","i":"IAuditableEntity LastModifiedBy IScheduledJob OutboxMessage TenantId Pii"},{"u":"/docs/adr/075-audit-trail.html#trade-offs","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Write amplification is real and it is on the caller's latency path. An entity with twenty changed properties writes twenty rows inside the caller's transaction, so an audited…","i":"IAuditTrailReader AddAuditTrail RetentionDays Pii"},{"u":"/docs/adr/075-audit-trail.html#related","d":"ADR-075: Audit Trail (Same-Transaction Field-Level Change History)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the same-transaction write this copies wholesale, including the retry-discard and the Add-only mutation rule), ADR-005 (soft-delete, [Pii] and erasure: why the trail…","i":"AuditTrailSettings RowVersion TenantId Add Pii"},{"u":"/docs/adr/076-data-subject-export.html","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/076-data-subject-export.html#status","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). Revised 2026-08-14 (the API-surface section corrected to the shipped mechanism, an abstract DataExportControllerBase a subclass mounts, not an…","i":"ExportUserDataHandlerBase DataExportControllerBase IUserDataExportSection"},{"u":"/docs/adr/076-data-subject-export.html#context","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Context","x":"A data-subject access request is a legal obligation with a clock on it: the person asks for a copy of the personal data held about them, and the operator has a deadline to hand…","i":"DeleteUserHandlerBase UserOwnershipRule IAnonymizable"},{"u":"/docs/adr/076-data-subject-export.html#decision","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Decision","x":"The framework takes the part that is the same in both apps; the app keeps the part that is not. A consumer's export handler becomes a subclass that supplies a role test and a set…","i":"AuthorizationPolicies.RequireAuthenticated EntitiesWithPiiImplementAnonymizable UserOwnershipRule.CheckOwnership AuditableAggregateRootEntity IUserEngagementExportService AddNotificationControllers PrivacyFeatures.DataExport AuthenticationServiceBase ExportUserDataHandlerBase DataExportControllerBase PiiEntitiesAreExportable IUserDataExportSection"},{"u":"/docs/adr/076-data-subject-export.html#rationale","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Rationale","x":"- The two halves of a handler have different owners. The ownership gate, the aggregate load, the fan-out, the per-section catch and the envelope are the same decisions in both…","i":"IUserEngagementExportService IUserSalesExportService ExportUserDataQuery UserOwnershipRule User"},{"u":"/docs/adr/076-data-subject-export.html#trade-offs","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- Best-effort degradation can return a quietly incomplete package. Available = false is the only signal, and nothing forces a caller, a UI, or the subject to read it. A section…","i":"DataExportControllerBase UserDataExportDTO UserOwnershipRule CurrentUserId FeatureGate Available false"},{"u":"/docs/adr/076-data-subject-export.html#related","d":"ADR-076: Data-Subject Export (DSAR) as a Framework Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (the erasure half of the same privacy obligation, whose IAnonymizable opt-in and [Pii] guard are this contract's mirror: one erases what the other copies), ADR-033 (the…","i":"PiiEntitiesAreExportable UserOwnershipRule IAnonymizable FeatureGate Result Pii"},{"u":"/docs/adr/077-hybridcache-substrate.html","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","i":"ICacheService"},{"u":"/docs/adr/077-hybridcache-substrate.html#status","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13). Amends ADR-026: Tier 1's substrate gains a third implementation beside MemoryCacheService and DistributedCacheService. It is opt-in through…","i":"OutputCacheEvictionRequested DistributedCacheService MMCA.Common.OutputCache AddCommonHybridCache MemoryCacheService"},{"u":"/docs/adr/077-hybridcache-substrate.html#context","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Context","x":"ADR-026 settled Tier 1 as one abstraction (ICacheService) over two implementations chosen at startup: in-process memory when no real IDistributedCache is present, Redis…","i":"Microsoft.Extensions.Caching.Hybrid ICacheService.IncrementAsync StackExchangeRedisCache IDistributedCache ICacheService HybridCache WRONGTYPE Result INCR"},{"u":"/docs/adr/077-hybridcache-substrate.html#decision","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Decision","x":"Ship HybridCacheService as a third ICacheService implementation, opt-in per host, under a disjoint keyspace. This is the structural rule the design is built around, and…","i":"HybridCacheEntryFlags.DisableUnderlyingData Microsoft.Extensions.Caching.Hybrid CacheOptions.DefaultDuration HybridCache.GetOrCreateAsync HybridCache.RemoveByTagAsync MMCA.Common.Infrastructure Directory.Packages.props DistributedCacheService DisableLocalCacheWrite IConnectionMultiplexer CachingQueryDecorator DisableLocalCacheRead"},{"u":"/docs/adr/077-hybridcache-substrate.html#rationale","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Rationale","x":"- The disjoint keyspace is the decision; everything else is implementation. Rather than trusting a second implementation to write a shape compatible with the first, this record…","i":"DisableUnderlyingData LocalCacheExpiration IncrementAsync GetAsync"},{"u":"/docs/adr/077-hybridcache-substrate.html#trade-offs","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Trade-offs","x":"- Invalidation does not reach other replicas' L1 immediately. A remove evicts the L2 entry and the calling replica's L1; every other replica keeps its copy for up to…","i":"AddCommonHybridCache LocalCacheExpiration GetOrCreateAsync IncrementAsync ICacheService RemoveAll"},{"u":"/docs/adr/077-hybridcache-substrate.html#related","d":"ADR-077: HybridCache as an Opt-In ICacheService Substrate (Amends ADR-026)","k":"Architecture Decision Records","t":"Related","x":"ADR-026 (amended by this record: its Tier 1 substrate gains a third implementation, its 30-second default TTL becomes the local-cache bound as well, its prefix-invalidation model…","i":"IncrementAsync GetAsync"},{"u":"/docs/adr/078-csv-export-endpoint.html","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records"},{"u":"/docs/adr/078-csv-export-endpoint.html#status","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-13; revised 2026-08-18). The implementation lands in the MMCA.Common \"enterprise capability wave\" release. Unlike the wave's other features this one is NOT…","i":"EntityControllerBase"},{"u":"/docs/adr/078-csv-export-endpoint.html#context","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Context","x":"The request is \"export what you filtered\". The generic entity surface of ADR-034 already accepts a full query vocabulary on the paged route…","i":"EntityQueryPipeline.MaxUnboundedResultLimit context.CacheVaryByRules.QueryKeys options.ReturnHttpNotAcceptable PublicEndpointOutputCachePolicy ReturnHttpNotAcceptable QueryFilterModelBinder IAsyncEnumerable OutputFormatter sortDirection sortColumn Accept AddAPI"},{"u":"/docs/adr/078-csv-export-endpoint.html#decision","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Decision","x":"EntityControllerBase gains a virtual [HttpGet(\"export\")] ExportAsync(...) action (Source/Presentation/MMCA.Common.API/Controllers/EntityControllerBase.cs). It accepts the same…","i":"QueryFieldService.ShapeCollectionData IPhysicalDbContextFactory.Create ApplicationSettings.MaxPageSize IEntityQueryService.GetAllAsync UnhandledResultFailureFilter JsonNamingPolicy.CamelCase ExportRowLimitHeaderName OpenApiContractTestsBase MaxUnboundedResultLimit QueryFilterModelBinder IEntityControllerBase EntityControllerBase"},{"u":"/docs/adr/078-csv-export-endpoint.html#rationale","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Rationale","x":"- A route is an unambiguous request; an Accept header is a preference. Given a cache policy that ignores Accept and a pipeline configured to never return 406, a client that…","i":"OutputFormatter Accept"},{"u":"/docs/adr/078-csv-export-endpoint.html#trade-offs","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Trade-offs","x":"- Every derivative gains a bulk read whether its owner wanted one or not. The only gate is the controller's existing authorization posture. A resource that was safe to page 20…","i":"GetExportSpecification MaxExportRows ExportAsync MaxPageSize Accept Skip Take"},{"u":"/docs/adr/078-csv-export-endpoint.html#related","d":"ADR-078: CSV Export as a Dedicated Endpoint, Not Content Negotiation","k":"Architecture Decision Records","t":"Related","x":"ADR-034 (the generic entity surface and query contract this extends, and the MaxUnboundedResultLimit ceiling that forced the page loop), ADR-040 (the output-cache policy whose…","i":"MaxUnboundedResultLimit Accept Result"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#status","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14). Revised 2026-08-19 (refreshed the WebApplicationBuilderExtensions.cs cross-reference anchor, which moved to :555). Revised 2026-08-21: the order became…","i":"MiddlewarePipelineBuilder.CreateDefault WebApplicationBuilderExtensions.cs MiddlewarePipelineOrderTestsBase"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#context","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Context","x":"In ASP.NET Core, middleware order is behavior, not style: a rate limiter placed before authentication partitions every request as anonymous, an HTTPS redirect placed in front of…","i":"TenantResolutionMiddleware SoftDeletedUserMiddleware UseAuthentication HttpContext.User"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#decision","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Decision","x":"Ship the edge as one ordered pipeline in the framework, UseCommonMiddlewarePipeline (MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:46), and…","i":"MiddlewarePipelineBuilder.CreateDefault MiddlewarePipelineOrderTestsBase DecoratorPipelineOrderTestsBase UseCommonRequestLocalization MiddlewarePipelineStepNames UseCommonMiddlewarePipeline TenantResolutionMiddleware ISoftDeletedUserValidator MiddlewarePipelineBuilder SoftDeletedUserMiddleware MiddlewarePipelineStep OidcDiscoveryEndpoint"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#rationale","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Rationale","x":"- Order is behavior, so it belongs to the framework, not to each host. Four of the adjacencies above fail silently when reversed: the limiter stops limiting, the tenant resolver…","i":"Program.cs"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#trade-offs","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Trade-offs","x":"- Two of this record's original costs are retired (2026-08-21). As accepted, nothing froze the order (no test referenced the method; the adjacencies were protected by comments…","i":"MiddlewarePipelineOrderTestsBase MiddlewarePipelineStepNames MapOidcDiscoveryEndpoint UseCommonSecurityHeaders PreForwardedCapture HttpContext.Items KnownIPNetworks InsertBefore KnownProxies PreForwarded Controllers jwks_uri"},{"u":"/docs/adr/079-shared-http-middleware-pipeline.html#related","d":"ADR-079: One Shared, Ordered HTTP Middleware Pipeline for Every Service Host","k":"Architecture Decision Records","t":"Related","x":"ADR-014 (the in-process sibling: one fixed decorator order for commands and queries), ADR-019 (depends on forwarded headers before the limiter and on the limiter after…"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#status","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#context","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Context","x":"Both production apps deploy to Azure Container Apps from a single deploy.yml job on push to main, and every gate runs before anything rolls out: the deploy job waits on…","i":"deploy.yml foundation deploy main"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#decision","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Decision","x":"Roll out one revision at a time, verify it from outside, and auto-revert the image only when the verification fails. - Single-revision rollout. Every container app runs…","i":"activeRevisionsMode rollback_failed containerapp createdTime Provisioned pipefail revision rollback failure sqlcmd probe Smoke"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#rationale","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Rationale","x":"- ARM success is the wrong success signal. The smoke gate converts \"the control plane accepted the template\" into \"the fleet answers requests\", which is the only claim a deploy…","i":"deploy"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#trade-offs","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Trade-offs","x":"- Schema is never rolled back, so a bad migration is fix-forward only. The image reverts and the database does not, so the previous release resumes against the new schema. This…","i":"rollback_failed Provisioned revision APPS copy"},{"u":"/docs/adr/080-deploy-rollout-revision-rollback.html#related","d":"ADR-080: Production Rollout with Automatic Revision-Only Rollback","k":"Architecture Decision Records","t":"Related","x":"ADR-057 (built on this model: revision-only rollback is why every migration must be backward compatible one release back), ADR-030 (startup migration as sole migrator, the reason…"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#status","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#context","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Context","x":"Both deployed apps run a deliberately small production footprint: every Container App is declared with maxReplicas: 2 and every SQL database with the Basic tier…","i":"maxReplicas Basic"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#decision","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Decision","x":"The cost baseline is asserted by a read-only reusable workflow that both runs weekly and sits in deploy.needs, so an un-reverted scale-up blocks the next production deploy. - One…","i":"properties.template.scale.maxReplicas BASELINE_MAX_REPLICAS AZURE_RESOURCE_GROUP github.event_name workflow_dispatch workflow_call deploy.needs environment release.yml main.bicep production MMCAStore"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#rationale","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Rationale","x":"- Configuration drift is the leading indicator; spend is the lagging one. The budget notification fires at 80% of actual spend, after the money is gone, and names a number rather…","i":"workflow_call deploy.needs maxReplicas deploy.yml sku.tier"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#trade-offs","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- A legitimate scale-up blocks deploys until the baseline is edited. Standing up extra capacity for a real event and then shipping a fix during it requires a pull request against…","i":"BASELINE_MAX_REPLICAS skip_freshness_gates skip_justification workflow_dispatch deploy.needs maxReplicas deploy.yml sku.tier Standard deploy Basic write"},{"u":"/docs/adr/081-cost-baseline-deploy-gate.html#related","d":"ADR-081: The Cost Baseline as a Hard Deploy Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-064 (the sibling deploy-precondition record, which decides the three proof-of-recency gates and enumerates this one only in passing; its break-glass input does not apply…","i":"deploy.needs"},{"u":"/docs/adr/082-two-tier-cors-posture.html","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records"},{"u":"/docs/adr/082-two-tier-cors-posture.html#status","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/082-two-tier-cors-posture.html#context","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Context","x":"Both deployed applications put a YARP gateway in front of per-module service hosts (ADR-008), and the browser and MAUI clients talk to the gateway origin while the services…"},{"u":"/docs/adr/082-two-tier-cors-posture.html#decision","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Decision","x":"Ship two cross-origin policies from the framework: an allow-listed one for service hosts and a deliberately broader one for gateways. - Service hosts register two named policies…","i":"CorsPolicyAllowSpecificOrigins app.Environment.IsDevelopment UseCommonMiddlewarePipeline Cors__AllowedOrigins__0 _allowSpecificOrigins AddCommonGatewayCors CorsPolicyAllowAll AddDefaultPolicy AllowCredentials IHostEnvironment AllowAnyHeader AllowAnyMethod"},{"u":"/docs/adr/082-two-tier-cors-posture.html#rationale","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Rationale","x":"- A proxy cannot allow-list what it does not own. The gateway has no controllers and no knowledge of which headers the fronted services accept, so a header allow-list there would…","i":"UseCommonMiddlewarePipeline AllowCredentials IHostEnvironment AllowAnyOrigin AddCommonCors UseCors"},{"u":"/docs/adr/082-two-tier-cors-posture.html#trade-offs","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Trade-offs","x":"- The gateway policy is broad on two of three axes. Any header and any method are accepted for an allow-listed origin. The origin list is the only lever there, so a mistake in…","i":"ProductionHostApplicationFactory IHostEnvironment.IsDevelopment configuration.GetSection ValidateOnStart UseEnvironment AddCommonCors UseCors string Get"},{"u":"/docs/adr/082-two-tier-cors-posture.html#related","d":"ADR-082: Two-Tier Cross-Origin Posture: Allow-Listed Service Policies, an Any-Header Gateway Policy","k":"Architecture Decision Records","t":"Related","x":"ADR-079 (the shared middleware pipeline whose fixed order places the environment-selected CORS policy between routing and authentication), ADR-008 (the gateway plus per-module…"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#status","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14). Revised 2026-08-23: the adopter counts were refreshed (ADC Conference's ActivityChanged joined the base-derived set) and three source citations were…","i":"ActivityChanged"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#context","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Context","x":"ADR-003 decides how a domain event moves: captured into the outbox inside SaveChangesAsync, dispatched in-process after commit, or published to the broker when it is an…","i":"SaveChangesAsync SessionChanged SessionCreated SessionDeleted Changed Created Deleted Session Entity"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#decision","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Decision","x":"Every generic CRUD lifecycle transition of an entity raises one event type for that entity, carrying a DomainEntityState discriminator; handlers filter on State. - One base…","i":"ProductVariantPriceChanged TicketChangedAuditHandler ProductVariantSkuChanged ShoppingCartItemChanged SessionQuestionChanged ShoppingCartCheckedOut ProductVariantRemoved SessionCreatedHandler BaseIntegrationEvent ProductVariantAdded EntityChangedEvent DomainEntityState"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#rationale","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Rationale","x":"- One type per entity is one subscription surface. A subscriber declares interest in the entity, then decides which transitions matter, instead of the container deciding for it…","i":"SessionChanged SessionCreated SessionDeleted OrderPaid"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#trade-offs","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Trade-offs","x":"- Every selective handler pays a filter. A handler that cares about one transition has to open with a State guard and return (SessionCreatedHandler.cs:17-18 is the shape to…","i":"EntityChangedEvent PointsEntryChanged BaseDomainEvent LivePollChanged LivePollStatus Unchanged Added State TId"},{"u":"/docs/adr/083-crud-lifecycle-event-taxonomy.html#related","d":"ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (how these events are captured and dispatched; this ADR decides only their shape), ADR-010 (schema versioning for the discriminator once it crosses a service boundary),…","i":"MessageId"},{"u":"/docs/adr/084-stripe-webhook-ingress.html","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#status","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-14)."},{"u":"/docs/adr/084-stripe-webhook-ingress.html#context","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Context","x":"Four ADRs already cover how a message crosses a boundary in this workspace. ADR-003 decides how an event leaves a service (outbox, at-least-once). ADR-021 decides how a…","i":"Warning"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#decision","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Decision","x":"Treat third-party webhook ingress as its own contract with two halves: an acceptance-coded endpoint and a self-registering, self-provisioning endpoint registration at startup. -…","i":"StripeWebhookRegistrationService EventUtility.ValidateSignature payment_intent.payment_failed AddModuleSalesInfrastructure SignatureVerificationFailed StripeWebhookSecretProvider checkout.session.completed throwOnApiVersionMismatch checkout.session.expired HttpContext.Request.Body EventUtility.ParseEvent additionalPortMappings"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#rationale","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Rationale","x":"- The caller's protocol decides the response vocabulary. Stripe reads a status code as \"keep retrying\" or \"stop\", not as \"this succeeded\" or \"this failed\". Mapping every…","i":"Critical Warning"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#trade-offs","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Trade-offs","x":"- A startup service that writes to a live third-party account. Booting a Sales replica creates and deletes webhook endpoints in the real Stripe account…","i":"StripeWebhookRegistrationService PaymentReconciliationService PaymentsController WebhookBaseUrl SecretKey Critical Warning"},{"u":"/docs/adr/084-stripe-webhook-ingress.html#related","d":"ADR-084: Stripe Webhook Ingress (Acceptance-Coded Responses and Startup Self-Registration)","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (outbound at-least-once delivery, the other end of the same family), ADR-021 (broker-side inbound dedup, which never sees a webhook), ADR-017 (client-supplied idempotency…"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#status","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Revised 2026-08-23 (the alias count and the migration-surface census were recounted, the census gained a stated methodology, and the CheckIn and…","i":"CheckIn"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#context","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Context","x":"ADR-048 decided that every entity identity is a primitive named through a per-module global using {Entity}IdentifierType = ... alias, and recorded the cost in one line of…","i":"SpeakerIdentifierType UserIdentifierType StronglyTypedId IdentifierType Notification System.Guid Entity global Source using Vogen and"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#decision","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Decision","x":"Keep the aliases. The wrapper-struct alternative is evaluated in this record, priced, and deferred again, this time against explicit triggers. Inside a module an identifier is…","i":"SessionIdentifierType SponsorIdentifierType EventIdentifierType UserIdentifierType checkedInByUserId IEntityDTOMapper TIdentifierType IdentifierType ValueConverter JsonConverter CheckInScope BaseEntity"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#rationale","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Rationale","x":"- The cost is paid once and the benefit accrues per defect avoided, and the defect count is currently zero. No production incident in any of the four repos has been traced to a…","i":"System.Text.Json Guid int"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#trade-offs","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The exposure is unmitigated, not reduced. This record buys no safety whatsoever. Every transposition ADR-048 could not catch is still uncatchable today, and the CheckIn…","i":"CheckIn.Create CheckIn Create int"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#related","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Related","x":"ADR-048 (the decision this record revisits and upholds; its Status now points here), ADR-068 (the deliberate opposite case: domain values carry invariants and therefore do get…"},{"u":"/docs/adr/085-identifier-type-aliases-revisited.html#revision-2026-08-23","d":"ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers)","k":"Architecture Decision Records","t":"Revision (2026-08-23)","x":"The decision, the priced alternative, the three triggers and the trade-offs are unchanged. What changed is arithmetic and three citations. The alias count is 44 across 10 files,…","i":"ActivityIdentifierType SpeakerIdentifierType IEntityDTOMapper TIdentifierType IdentifierType CheckInScope System.Guid TEntityDTO sponsorId IBaseDTO CheckIn TEntity"},{"u":"/docs/adr/086-process-manager-deferred.html","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records"},{"u":"/docs/adr/086-process-manager-deferred.html#status","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18) as a documented deferral. Nothing ships with this record: no state machine, no correlation store, no new package. What ships is the shape the coordinator…"},{"u":"/docs/adr/086-process-manager-deferred.html#context","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Context","x":"ADR-054 decided how this workspace achieves cross-boundary consistency without two-phase commit: choreography. Each step of a workflow raises a domain event, each compensating…","i":"PaymentReconciliationService PeriodicBackgroundService SagaStateMachineInstance MassTransitStateMachine Order.InventoryRestored InMemorySagaRepository Order.Status SaveChanges Source ISaga"},{"u":"/docs/adr/086-process-manager-deferred.html#decision","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Decision","x":"Defer the process manager, and record its shape so the deferral is a design decision rather than an omission. A durable multi-step workflow coordinator in this workspace is a…","i":"MassTransit.Azure.ServiceBus.Core MassTransitStateMachine MassTransit.RabbitMQ CorrelationId MassTransit InProcess TInstance Result"},{"u":"/docs/adr/086-process-manager-deferred.html#rationale","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Rationale","x":"- Choreography is genuinely correct for the workflow that exists. This is not a case of the simpler option being tolerated. Checkout's saga state is two fields on Order, and an…","i":"Order"},{"u":"/docs/adr/086-process-manager-deferred.html#trade-offs","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The first workflow to hit the trigger pays the full cost at once, under whatever deadline made it appear. Deferral moves the work onto the critical path of the feature that…","i":"SQLServerDbContext"},{"u":"/docs/adr/086-process-manager-deferred.html#related","d":"ADR-086: A Process Manager Is Deferred, Not Absent (Relates to ADR-054)","k":"Architecture Decision Records","t":"Related","x":"ADR-054 (the accepted mechanism this record defers an alternative to: choreographed compensation, the persisted aggregate marker, and the reconciliation sweep that would remain…"},{"u":"/docs/adr/087-broker-poison-message-handling.html","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records"},{"u":"/docs/adr/087-broker-poison-message-handling.html#status","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Amends ADR-009: the outbox's broker publish gains a circuit breaker, which is the first resilience policy this workspace applies to something other than an…"},{"u":"/docs/adr/087-broker-poison-message-handling.html#context","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Context","x":"Delivery in this workspace has always been at-least-once with retries on both legs: the outbox retries a failed publish with jittered exponential backoff and eventually…","i":"rabbitmq_delayed_message_exchange DeadLetterRetentionDays"},{"u":"/docs/adr/087-broker-poison-message-handling.html#decision","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Decision","x":"Three changes, each scoped to one failure: second-level redelivery configured per transport, a fault consumer with its own meter, and a circuit breaker around the outbox's broker…","i":"RegisterIntegrationEventConsumer settings.EnableDelayedRedelivery FaultIntegrationEventConsumer OperationCanceledException RedeliveryIntervalsSeconds broker.circuit.open.count BuildRedeliveryIntervals cfg.UseDelayedRedelivery ConfigureBrokerTransport EnableDelayedRedelivery BrokenCircuitException fault.FaultedMessageId"},{"u":"/docs/adr/087-broker-poison-message-handling.html#rationale","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Rationale","x":"- The transport asymmetry follows a real capability difference, not a preference. RabbitMQ needs a plugin the dev container lacks; Service Bus does not. A single default would be…","i":"BrokenCircuitException true"},{"u":"/docs/adr/087-broker-poison-message-handling.html#trade-offs","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Trade-offs","x":"- Delayed redelivery is off where the plugin problem lives. RabbitMQ is the local transport and also a plausible self-hosted production transport; both get default-off, so the…","i":"RegisterIntegrationEventConsumer RedeliveryIntervalsSeconds broker.fault.count MMCA.Common.Aspire BrokerMetrics internal"},{"u":"/docs/adr/087-broker-poison-message-handling.html#related","d":"ADR-087: Broker Poison-Message Handling: Second-Level Redelivery and Fault Observability","k":"Architecture Decision Records","t":"Related","x":"ADR-003 (the outbox publish leg this breaker wraps, and the retry, jittered backoff and dead-lettering that BrokenCircuitException reuses unchanged), ADR-066 (the transport…","i":"RedeliveryIntervalsSeconds BrokenCircuitException MMCA.Common.Broker MMCA.Common.Outbox MMCA.Common.Cqrs"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#status","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18). Extends ADR-019 with a fourth, edge-tier layer whose posture is the deliberate opposite of the service tier's authenticated-only global limiter; nothing in…"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#context","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Context","x":"ADR-008 made the Gateway the only client entry point and gave it three jobs: the route-to-service map, CORS, and forwarding the caller's Authorization header. Nothing was added…","i":"CorrelationIdMiddleware AddCommonRateLimiting MMCA.Common.Aspire MMCA.Common.API Authorization"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#decision","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Decision","x":"Ship a gateway edge kit in a Gateway namespace inside MMCA.Common.Aspire, owning exactly three responsibilities, and record three more as deliberately declined. 1. Correlation is…","i":"PartitionedRateLimiter.CreateChained AddGatewayDownstreamHealthChecks RateLimitPartition.GetNoLimiter GatewayCorrelationMiddleware GatewayRateLimitingSettings HttpContext.TraceIdentifier Connection.RemoteIpAddress Validator.ValidateObject ValidateDataAnnotations AddGatewayRateLimiting GlobalConcurrencyLimit UseGatewayCorrelation"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#rationale","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Rationale","x":"- The edge is the only place that sees a request exactly once. That is what makes ensure-at-the-edge correct and mint-per-service wrong: not that the service version is broken,…","i":"Ready Live"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#trade-offs","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Trade-offs","x":"- The limiter closes over an eagerly-bound copy of the settings (GatewayRateLimitingExtensions.cs:154, consumed at :164-172), so an IOptionsMonitor reload never reaches it.…","i":"BypassPathPrefixes IOptionsMonitor PermitLimit IOptions"},{"u":"/docs/adr/088-gateway-edge-responsibilities.html#related","d":"ADR-088: Gateway Edge Responsibilities (and the Three It Declines)","k":"Architecture Decision Records","t":"Related","x":"ADR-008 (the record that made the Gateway the only entry point and gave it routing, CORS and auth forwarding; this is the first record to add cross-cutting behavior to it),…"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#status","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-18; revised 2026-08-23: ADC's table gained a 27th route, /Activities, on 2026-08-19, and the bicep anchors below are corrected). Amends ADR-008: that record…","i":"MapForwarder ReverseProxy"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#context","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Context","x":"Before this record, both gateways built their route table by hand, in code. ADC made 26 MapForwarder calls and Store made 10. Neither host called AddReverseProxy or…","i":"HttpResilienceDefaults.TotalRequestTimeout AddServiceDiscoveryDestinationResolver ForwarderRequestConfig.ActivityTimeout AddHttpForwarderWithServiceDiscovery ForwarderRequestConfig HttpVersion.Version20 RequestVersionExact appsettings.json AddReverseProxy IHttpForwarder LoadFromConfig ForwardHttp2"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#decision","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Decision","x":"Make configuration the single source of the gateway route table, and pin it with a test. Each gateway calls…","i":"HttpResilienceDefaults.TotalRequestTimeout Http2ForwardingConfigFilter RequestVersionExact appsettings.json MapReverseProxy IHttpForwarder RouteMapTests ForwardHttp2 IProxyConfig MapForwarder ReverseProxy HttpRequest"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#rationale","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Rationale","x":"- The drift already happened, in the repository that has the most gateway tests. ADC is the careful consumer, and it still carried three unpinned routes, an off-by-one comment…"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#trade-offs","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Trade-offs","x":"- The compiler stopped helping. A misspelled cluster reference, a malformed path pattern or a route that shadows another is a runtime failure, discovered as a 404 or a 502, where…","i":"appsettings.json ForwardHttp2 IProxyConfig MapForwarder Order"},{"u":"/docs/adr/089-gateway-topology-owned-by-configuration.html#related","d":"ADR-089: Gateway Topology Owned by Configuration","k":"Architecture Decision Records","t":"Related","x":"ADR-008 (amended: the Gateway keeps the route-to-service map it was given, now expressed as configuration rather than as forwarder registrations), ADR-088 (the other half of this…","i":"HttpResilienceDefaults.TotalRequestTimeout"},{"u":"/docs/adr/090-event-upcaster-registration.html","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records"},{"u":"/docs/adr/090-event-upcaster-registration.html#status","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-21). Completes the follow-up named in ADR-010: that record established the versioning policy (a SchemaVersion signal plus a new-type-and-upcaster discipline for…","i":"SchemaVersion"},{"u":"/docs/adr/090-event-upcaster-registration.html#context","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Context","x":"ADR-010 splits event evolution into a signal and a discipline. The signal (SchemaVersion, a fitness-function-gated property on every integration event) shipped with ADR-010…","i":"InProcessMessageBus SchemaVersion"},{"u":"/docs/adr/090-event-upcaster-registration.html#decision","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Decision","x":"1. A typed upcaster abstraction, in the Application layer. IEventUpcaster (Source/Core/MMCA.Common.Application/Interfaces/IEventUpcaster.cs) is a pure payload mapping from a…","i":"RegisterUpcastedIntegrationEventConsumer EventUpcastersHaveUniqueSourceTypes EventUpcastersIncreaseSchemaVersion UpcastingIntegrationEventConsumer ArchitectureRules.Upcasters.cs services.AddEventUpcaster AddUserDataExportSection EventConventionTestsBase IIntegrationEventHandler IntegrationEventConsumer DomainEventDispatcher EventUpcasterRegistry"},{"u":"/docs/adr/090-event-upcaster-registration.html#rationale","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Rationale","x":"- The discipline becomes a mechanism. ADR-010's own framing (a thing that matters is a check, not a comment) now applies to the upcaster half: the transform has a first-class…","i":"SchemaVersion MessageId"},{"u":"/docs/adr/090-event-upcaster-registration.html#trade-offs","d":"ADR-090: Event Upcaster Registration Extension Point","k":"Architecture Decision Records","t":"Trade-offs","x":"- The upcast walk advances by declared target type, not runtime type. A misbehaving upcaster that returns an instance of some other type cannot send the walk into an unvalidated…","i":"UpcastingIntegrationEventConsumer OutboxMessage.DeserializeEvent OutputCacheEvictionRequested DomainEventDispatcher AddEventUpcaster"},{"u":"/docs/adr/091-cache-backed-password-reset.html","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records"},{"u":"/docs/adr/091-cache-backed-password-reset.html#status","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-22). Extends ADR-029 (the cache-backed login-protection idiom this record reuses) and ADR-032 (which decided how a password is stored, never how a user who has…"},{"u":"/docs/adr/091-cache-backed-password-reset.html#context","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Context","x":"Both consumer apps shipped authenticated password change (PUT /Auth/password) and nothing for a user who cannot sign in at all. The recorded fallback in MMCA.ADC's specification…","i":"ResetTokenExpiresAt ResetTokenHash ResetAttempts PUT"},{"u":"/docs/adr/091-cache-backed-password-reset.html#decision","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Decision","x":"1. The reset token is a cache record, not a schema change. IPasswordResetTokenService (Source/Core/MMCA.Common.Application/Auth/IPasswordResetTokenService.cs) is two methods,…","i":"CryptographicOperations.FixedTimeEquals PasswordResetAuthControllerBase IPasswordResetTokenService FindUntrackedByEmailAsync ForgotPasswordHandlerBase ResetPasswordHandlerBase PasswordReset__ResetUrl PasswordResetController ValidateAndConsumeAsync Auth.InvalidResetToken LoginProtectionService ForgotPasswordCommand"},{"u":"/docs/adr/091-cache-backed-password-reset.html#rationale","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Rationale","x":"- No migration is the whole point. A reset credential is short-lived by nature, and the expiry semantics a reset needs (a TTL, a single use, an attempt cap) are native to a cache…","i":"Result.Success"},{"u":"/docs/adr/091-cache-backed-password-reset.html#trade-offs","d":"ADR-091: Cache-Backed Password Reset","k":"Architecture Decision Records","t":"Trade-offs","x":"- Cache eviction invalidates outstanding tokens. A Redis restart, an eviction under memory pressure, or a fall back to the in-memory store on a different replica all silently…","i":"IncrementAsync"},{"u":"/docs/adr/092-web-vitals-budget-gate.html","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records"},{"u":"/docs/adr/092-web-vitals-budget-gate.html#status","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-23)."},{"u":"/docs/adr/092-web-vitals-budget-gate.html#context","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records","t":"Context","x":"Rubric section 23 asks for client-side performance that is measured rather than assumed, naming Core Web Vitals (LCP, INP, CLS) or an equivalent as the evidence…"},{"u":"/docs/adr/092-web-vitals-budget-gate.html#decision","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records","t":"Decision","x":"Ship the measurement infrastructure and the assert mechanics in MMCA.Common.Testing.E2E, default the budget to the Core Web Vitals good band, and let the assertions ride the…","i":"MMCA.Common.Testing.E2E WEB_VITALS_OUTPUT_DIR WebVitalsBudgetTests BeLessThanOrEqualTo PerformanceObserver AssertWithinBudget WebVitalsCollector WriteArtifactAsync durationThreshold WebVitalsArtifact WebVitalsE2ETests E2E.Tests.csproj"},{"u":"/docs/adr/092-web-vitals-budget-gate.html#rationale","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records","t":"Rationale","x":"- The good band is an external contract, which is what makes an absolute ceiling defensible here. ADR-060 refused absolute latency because a nanosecond count is a property of the…"},{"u":"/docs/adr/092-web-vitals-budget-gate.html#trade-offs","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records","t":"Trade-offs","x":"- The gate is ui-scoped and may legitimately skip. Both apps gate e2e-gate on a ui change filter (ADC deploy.yml:538, Store :544) and deploy accepts skipped for it (ADC :896,…","i":"WEB_VITALS_OUTPUT_DIR InteractiveServer InteractiveAuto skipped deploy"},{"u":"/docs/adr/092-web-vitals-budget-gate.html#related","d":"ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate","k":"Architecture Decision Records","t":"Related","x":"ADR-063 (the structural sibling: the same package, the same Playwright suite and the same deploy gate, applied to WCAG 2.1 AA instead of load performance), ADR-060 (the backend…"},{"u":"/docs/adr/093-container-image-posture.html","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records"},{"u":"/docs/adr/093-container-image-posture.html#status","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-23) for the three build decisions below. The two runtime postures in \"Open postures\" are recorded as undecided: they describe what the images do today and the…"},{"u":"/docs/adr/093-container-image-posture.html#context","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Context","x":"Eleven Dockerfiles produce every deployable container in the two Azure-hosted applications: six in MMCA.ADC (four services, the Gateway, the Blazor web host) and five in…","i":"publish latest build final base COPY"},{"u":"/docs/adr/093-container-image-posture.html#decision","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Decision","x":"1. The GitHub Packages credential is a BuildKit secret, never an ARG or ENV. Both applications' nuget.config source-maps MMCA. to GitHub Packages, so every restore inside an…","i":"Directory.Packages.props TreatWarningsAsErrors GITHUB_TOKEN nuget.config ENTRYPOINT history publish secrets docker dotnet build final"},{"u":"/docs/adr/093-container-image-posture.html#open-postures-undecided","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Open postures (undecided)","x":"The base image is a floating tag, not a digest. All eleven images start from mcr.microsoft.com/dotnet/aspnet:10.0 with no digest pin (.../MMCA.ADC.Conference.Service/Dockerfile:1…","i":"aspnet latest final USER app"},{"u":"/docs/adr/093-container-image-posture.html#rationale","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Rationale","x":"- A secret that is never a layer cannot leak from a layer. BuildKit secret mounts are the only mechanism that keeps the credential out of the image, the build cache and docker…","i":"history docker ARG ENV RUN"},{"u":"/docs/adr/093-container-image-posture.html#trade-offs","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Trade-offs","x":"- Eleven copies drift independently. There is no shared base Dockerfile and no test that compares them, so a fix applied to one image is applied to one image. The ReadyToRun…","i":"csproj"},{"u":"/docs/adr/093-container-image-posture.html#related","d":"ADR-093: Container Image Build and Runtime Posture","k":"Architecture Decision Records","t":"Related","x":"ADR-038 (supply-chain provenance: it gates the package graph with lock files, a vulnerability audit and an SBOM, and stops at the repository boundary, so the image layers this…"},{"u":"/docs/adr/094-client-entity-data-access.html","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/094-client-entity-data-access.html#status","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-23)."},{"u":"/docs/adr/094-client-entity-data-access.html#context","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records","t":"Context","x":"ADR-034 decided the server half of entity data access: a generic controller base with a dynamic query contract, where filters arrive as filters[Property].operator /…","i":"QueryFilterModelBinder MMCA.Common.UI operator Property filters value"},{"u":"/docs/adr/094-client-entity-data-access.html#decision","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records","t":"Decision","x":"Client-side entity data access goes through one hand-written base hierarchy in MMCA.Common.UI. - One HTTP root: AuthenticatedServiceBase…","i":"DomainInvariantViolationException IdempotencyHeaders.IdempotencyKey EntityServiceBase.GetPagedAsync CreateAuthenticatedClientAsync ResetCancellationTokenAsync ListPageQueryStateService AuthenticatedServiceBase CultureDelegatingHandler Directory.Packages.props PersistentComponentState EnsureSuccessStatusCode ObjectDisposedException"},{"u":"/docs/adr/094-client-entity-data-access.html#rationale","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records","t":"Rationale","x":"- A hand-written typed base beats a generated client here because the surface is already generic. ADR-034 collapsed N entity endpoints into one shape, so there is exactly one…","i":"EnsureSuccessStatusCode"},{"u":"/docs/adr/094-client-entity-data-access.html#trade-offs","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- No generated client means drift is caught at runtime, not at build time. A server-side rename of a query parameter or a DTO property does not fail the UI build; it fails the…","i":"ChildEntityServiceBase.PostAsync ConfigureHttpClientDefaults EntityServiceBaseTests.cs AuthenticatedServiceBase ChildEntityServiceBase AddServiceDefaults CartStateService RetryPolicy protected AddAsync readonly static"},{"u":"/docs/adr/094-client-entity-data-access.html#related","d":"ADR-094: Client-Side Entity Data-Access Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-034 (the server surface this contract calls, and the filter grammar the client constructs), ADR-017 (the server-side filter whose client half is specified here: who mints the…","i":"DataGridListPageBase TDto"},{"u":"/docs/adr/095-soft-delete-unique-indexes.html","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records"},{"u":"/docs/adr/095-soft-delete-unique-indexes.html#status","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-23)."},{"u":"/docs/adr/095-soft-delete-unique-indexes.html#context","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records","t":"Context","x":"ADR-005 makes deletion soft: an IAuditableEntity sets IsDeleted = true (MMCA.Common/Source/Core/MMCA.Common.Domain/Interfaces/IAuditableEntity.cs:11) and a named global query…","i":"IAuditableEntity HasFilter IsDeleted true"},{"u":"/docs/adr/095-soft-delete-unique-indexes.html#decision","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records","t":"Decision","x":"Make the filter a convention: every unique index on a soft-deletable entity excludes deleted rows, automatically, in every context of every consumer. - A model-finalizing…","i":"ApplicationDbContext.ConfigureConventions SoftDeleteUniqueIndexConvention SoftDeleteFilterSql.Build DataSource.CosmosDB HasSoftDeleteFilter additionalFilter IAuditableEntity HasColumnName filter null AND"},{"u":"/docs/adr/095-soft-delete-unique-indexes.html#rationale","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records","t":"Rationale","x":"- The database should agree with what the application shows. The query filter already says a soft-deleted row does not exist; a unique index that disagrees is the one place the…","i":"DedupKey Build NULL NOT"},{"u":"/docs/adr/095-soft-delete-unique-indexes.html#trade-offs","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records","t":"Trade-offs","x":"- It moves schema in consumers, invisibly from the entity configuration. Adopting the convention is a database-contract change: nothing in an entity configuration changed, but…","i":"IX_CategoryItem_CategoryId_Name ignoreQueryFilters builder.HasIndex index.GetFilter IX_User_Email IsDeleted IsUnique x.Email false true"},{"u":"/docs/adr/095-soft-delete-unique-indexes.html#related","d":"ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes)","k":"Architecture Decision Records","t":"Related","x":"ADR-005 (decides soft-delete over erasure and owns the query filter that hides the row, but says nothing about uniqueness: this ADR closes that gap), ADR-057 (the expand/contract…","i":"ApplicationDbContext"},{"u":"/docs/adr/096-best-effort-side-effects.html","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records"},{"u":"/docs/adr/096-best-effort-side-effects.html#status","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records","t":"Status","x":"Accepted (2026-08-23)."},{"u":"/docs/adr/096-best-effort-side-effects.html#context","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records","t":"Context","x":"A command that has already committed often has follow-up work attached to it: evict the output-cache entries the write invalidated, broadcast the new state to a live channel,…","i":"MarkAsFailed Exception catch"},{"u":"/docs/adr/096-best-effort-side-effects.html#decision","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records","t":"Decision","x":"One framework helper defines the contract, and a swallow that does not go through it is a deliberate, documented exception. - BestEffort.ExecuteAsync(operation, logger, action,…","i":"BestEffortLog.DispatchFailed besteffort.dispatch.failed OperationCanceledException OutputCacheEvictionHandler BestEffort.ExecuteAsync MMCA.Common.OutputCache CancellationToken.None MMCA.Common.BestEffort ArgumentNullException cache.eviction.failed ProductVariantChanged TryEvictByTagAsync"},{"u":"/docs/adr/096-best-effort-side-effects.html#rationale","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records","t":"Rationale","x":"- One policy beats five local leniencies. Each feature record is still right about its own degradation; what they could not each decide is the shape of the swallow. A single…","i":"AddVariantHandler"},{"u":"/docs/adr/096-best-effort-side-effects.html#trade-offs","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records","t":"Trade-offs","x":"- Nothing gates use of the helper. There is no fitness rule, analyzer or architecture test that fails a build for a hand-rolled catch (Exception) that should have been a…","i":"besteffort.dispatch.failed cache.eviction.failed SubmitQuestionHandler MMCA.Common.Aspire BestEffort Exception catch"},{"u":"/docs/adr/096-best-effort-side-effects.html#related","d":"ADR-096: Best-Effort Side-Effect Contract","k":"Architecture Decision Records","t":"Related","x":"ADR-024 (push delivery failure is non-fatal and recorded rather than raised, one of the local leniencies this policy generalizes), ADR-026 (eviction is best-effort, and its…","i":"besteffort.dispatch.failed OutputCacheEvictionHandler"},{"u":"/docs/onboarding/index.html","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","x":"A teaching guide for an experienced .NET engineer who is new to this codebase. It walks every first-party type, explaining not just what each type is but how it works and why it…","i":"CLAUDE.md dotnet new"},{"u":"/docs/onboarding/index.html#how-the-guide-is-organized-two-axes","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"How the guide is organized, two axes","x":"The guide has two organizing axes that work together. 1. Primary axis, functional grouping. Every type lives in exactly one functional group: the capability or cross-cutting…","i":"SelfHttpWarmupTask GateTestContext MMCA.Common MMCA.ADC Priority"},{"u":"/docs/onboarding/index.html#chapters","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"Chapters","x":"---","i":"AuthenticationServiceBase HttpResilienceDefaults AuthenticationService ConferencePermissions ApplicationDbContext IdentityPermissions SQLServerDbContext HealthCheckTags OutboxFinalizer HasPermission ThemeService Contracts"},{"u":"/docs/onboarding/index.html#legend-how-to-read-a-type-section","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"Legend, how to read a type section","x":"Every type gets one section using this template: {TypeName} {Assembly} · {namespace} · {file:line} · Level {n} · {kind} - What it is: one or two plain-language sentences. -…","i":"namespace Result Rubric Name"},{"u":"/docs/onboarding/index.html#suggested-reading-paths","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"Suggested reading paths","x":"- Framework-first (recommended). Primer → group-01 → upward. You meet the MMCA.Common foundations before the MMCA.ADC features that build on them; this matches dependency order…","i":"MMCA.Common MMCA.ADC Rubric"},{"u":"/docs/onboarding/index.html#the-companion-projects-context","d":"MMCA.Common + MMCA.ADC, Onboarding Guide","k":"Onboarding Guide","t":"The companion projects (context)","x":"This guide covers MMCA.Common (the framework) and MMCA.ADC (one consumer). MMCA.Store is out of scope. The dependency arrow is why the Common framework groups (1–16) come before…","i":"MMCA.Store"},{"u":"/docs/onboarding/00-dependency-manifest.html","d":"Phase 1: Dependency Manifest & Leveling","k":"Onboarding Guide","x":"Each distinct type node is assigned a Level by longest-path layering over its first-party dependencies (base/interface, generic constraints, field/property/param/return types,…","i":"System.Guid global static using Using int"},{"u":"/docs/onboarding/00-dependency-manifest.html#manifest-by-level-then-assembly","d":"Phase 1: Dependency Manifest & Leveling","k":"Onboarding Guide","t":"Manifest (by level, then assembly)","i":"DefaultEntityConfigurationAssemblyProviderTests GetPublicSessionCategoryItemFilterHandlerTests GetPublicSpeakerCategoryItemFilterHandlerTests AddSessionQuestionAnswerCommandValidatorTests ConferenceCategoryCreateRequestValidatorTests ConferenceCategoryUpdateRequestValidatorTests UserNotificationExportServiceGrpcAdapterTests MarkAllNotificationsReadHandlerTrackingTests SignalRPushNotificationSenderAdditionalTests UserSessionBookmarkCacheEvictionHandlerTests AddEventQuestionAnswerCommandValidatorTests AddSessionCategoryItemCommandValidatorTests"},{"u":"/docs/onboarding/00-group-taxonomy.html","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","x":"This is the primary axis of the guide. Every one of the 3,465 distinct first-party type nodes from 00-inventory.md is assigned to exactly one functional group - its primary home:…","i":"MMCA.Common MMCA.ADC Result"},{"u":"/docs/onboarding/00-group-taxonomy.html#design-notes-boundary-decisions-worth-stating-up-front","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","t":"Design notes (boundary decisions worth stating up front)","x":"- Cycles are kept whole. The 13 dependency cycles (SCCs) from the manifest are never split across groups. Notably the ApplicationDbContext AuditSaveChangesInterceptor…","i":"DomainEventSaveChangesInterceptor DataSourceModelCacheKeyFactory AuditSaveChangesInterceptor MMCA.ADC.Notification ApplicationDbContext MMCA.Common.Testing IAnonymizable PiiAttribute Gallery Rubric Fact S30"},{"u":"/docs/onboarding/00-group-taxonomy.html#the-groups-ordered","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","t":"The groups (ordered)","x":"Reconciliation: 1685 production types across 26 groups + 1780 test/testing types in G25 = 3465 (matches the inventory's distinct-node count). No type appears twice; none dropped.…"},{"u":"/docs/onboarding/00-group-taxonomy.html#group-membership","d":"Phase 1b - Functional Group Taxonomy","k":"Onboarding Guide","t":"Group membership","x":"group-01-result-error-handling.md 14 types The Result/Error railway that every operation returns instead of throwing; pagination result shapes. group-02-domain-building-blocks.md…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests SessionBookmarkValidationServiceGrpcAdapter DefaultEntityConfigurationAssemblyProvider GetPublicSessionCategoryItemFilterHandler GetPublicSpeakerCategoryItemFilterHandler SessionQuestionPendingCountChangedPayload AddSessionQuestionAnswerCommandValidator ConferenceCategoryCreateRequestValidator ConferenceCategoryUpdateRequestValidator CookieSessionRefreshMiddlewareExtensions DisabledSessionBookmarkValidationService MMCA.ADC.Conference.Infrastructure.Tests"},{"u":"/docs/onboarding/00-inventory.html","d":"Phase 0: Type Inventory","k":"Onboarding Guide","x":"Generated mechanically by a Roslyn syntactic parse of every in-scope .cs file under MMCA.Common/Source, MMCA.Common/Tests, MMCA.ADC/Source, MMCA.ADC/Tests. - Files scanned: 2810…","i":"extension"},{"u":"/docs/onboarding/00-inventory.html#full-inventory","d":"Phase 0: Type Inventory","k":"Onboarding Guide","t":"Full inventory","i":"MMCA.ADC.Conference.Application.Events.Sessionize MMCA.ADC.Conference.Application.Events.Validation MMCA.ADC.Conference.Application.Tests.Events.DTOs MMCA.ADC.Conference.Domain.Questions.DomainEvents MMCA.ADC.Conference.Infrastructure.Tests.Services MMCA.ADC.Conference.IntegrationTests.CrossService MMCA.ADC.Engagement.Application.CheckIns.Services MMCA.ADC.Engagement.Domain.LivePolls.DomainEvents MMCA.ADC.Engagement.Domain.Tests.SessionQuestions MMCA.ADC.Identity.IntegrationTests.Infrastructure MMCA.Common.Application.Interfaces.Infrastructure MMCA.Common.Application.Users.UseCases.DeleteUser"},{"u":"/docs/onboarding/00-inventory.html#extensiont-blocks","d":"Phase 0: Type Inventory","k":"Onboarding Guide","t":"extension(T) blocks","i":"IDistributedApplicationBuilder IBusRegistrationConfigurator AuthenticationBuilder IEndpointRouteBuilder WebApplicationBuilder IApplicationBuilder ICurrentUserService IReadOnlyCollection currentUserService IServiceCollection OutputCacheOptions IResourceBuilder"},{"u":"/docs/onboarding/00-inventory.html#generated--excluded-artifacts-no-type-sections-written","d":"Phase 0: Type Inventory","k":"Onboarding Guide","t":"Generated / excluded artifacts (no type sections written)","x":"118 files excluded as generated (EF migrations, snapshots, .g.cs, AssemblyInfo)."},{"u":"/docs/onboarding/00-primer.html","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","x":"This chapter teaches the cross-cutting things once, so the per-type chapters can stay focused. Read it before the group chapters (start with group-01). Everything here is either…"},{"u":"/docs/onboarding/00-primer.html#1-the-big-picture","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"1. The big picture","x":"Two codebases are in scope: - MMCA.Common: a framework, published as fifteen NuGet packages to nuget.org (the documented install path) and mirrored to GitHub Packages (ADR-053)…","i":"Testing.Architecture Aspire.Hosting Infrastructure Application MMCA.Common Testing.E2E references Testing.UI MMCA.ADC Testing UI.Maui Aspire"},{"u":"/docs/onboarding/00-primer.html#2-architectural-styles-this-codebase-commits-to","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"2. Architectural styles this codebase commits to","x":"These are the recurring ideas. Each is taught fully at its first concrete appearance in a group chapter; here is the orientation so the vocabulary is familiar. - Domain-Driven…","i":"EntityTypeConfigurationSQLServer PublicEndpointOutputCachePolicy JwtForwardingClientInterceptor FaultIntegrationEventConsumer GatewayCorrelationMiddleware JwtSettings.SigningAlgorithm EntityTypeConfigurationBase UseCommonMiddlewarePipeline TenantResolutionMiddleware ExportUserDataHandlerBase ISoftDeletedUserValidator ServiceInfoControllerBase"},{"u":"/docs/onboarding/00-primer.html#3-the-external-stack-bcl--nuget-external-level-0","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"3. The external stack (BCL / NuGet, \"external Level 0\")","x":"These are not first-party and get no per-type sections. Versions are from MMCA.Common/Directory.Packages.props and MMCA.ADC/Directory.Packages.props (Central Package Management,…","i":"Microsoft.Extensions.ServiceDiscovery.Yarp Microsoft.Extensions.Http.Resilience Notification.PushNotifications IEntityTypeConfiguration MMCA.Common.UI global.json IMessageBus SaveChanges TryDecorate DbContext OrderBy vX.Y.Z"},{"u":"/docs/onboarding/00-primer.html#4-c-build-and-code-style-conventions","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"4. C#, build, and code-style conventions","x":"- .NET 10.0, LangVersion: preview: required because the codebase uses C extension types (extension(T) syntax, see below). - Central Package Management (CPM). All NuGet versions…","i":"csharp_style_namespace_declarations MMCA.Common.Testing.Architecture ManagePackageVersionsCentrally Directory.Packages.props DependencyInjection.cs DependencyVersionTests TreatWarningsAsErrors csharp_prefer_braces EntityTypeExtensions packageSourceMapping IServiceCollection IArchitectureMap"},{"u":"/docs/onboarding/00-primer.html#5-the-solution--test-layout","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"5. The solution / test layout","x":"- .slnx: the human solution (XML format). .slnf, a solution filter used in CI to build a subset fast (MMCA.Store.CI.slnf, MMCA.ADC.CI.slnf). - Microsoft Testing Platform, not…","i":"MMCA.Common.UI.E2E.Tests MMCA.Common.UI.Gallery MMCA.Store.CI.slnf MMCA.ADC.CI.slnf csproj slnx"},{"u":"/docs/onboarding/00-primer.html#6-the-34-category-architecture-evaluation-lens","d":"Primer, the concepts, stack, and conventions you need first","k":"Onboarding Guide","t":"6. The 34-category architecture-evaluation lens","x":"This codebase is also scored against a 34-category rubric (Website/docs-src/governance/ArchitectureEvaluationCriteria.md, published at ). This guide weaves the rubric in so you…","i":"Rubric Name"},{"u":"/docs/onboarding/group-01-result-error-handling.html","d":"1. Result & Error Handling","k":"Onboarding Guide","x":"This is the first capability chapter, and it is deliberately first because the pattern it teaches underpins almost every other one in the guide. Before you read a command…","i":"ArgumentOutOfRangeException.ThrowIfNegative ArgumentNullException.ThrowIfNull DomainInvariantViolationException MMCA.Common.Shared.Serialization MMCA.Common.Shared.Abstractions System.Text.Json.Utf8JsonReader GrpcResultExceptionInterceptor System.Text.Json.Serialization MMCA.Common.Shared.Exceptions System.Buffers.Text.Base64Url Base64Url.TryDecodeFromChars ValidationFailureExtensions"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","x":"What this group covers. This is the DDD heart of the framework, the small, dependency-light primitives every business model in MMCA.Common and MMCA.ADC is built from. There are…","i":"EnumerationJsonConverterFactory AuditableAggregateRootEntity IdValueGeneratedAttribute CurrencyJsonConverter PhoneNumberInvariants EntityTypeExtensions EnumerationConverter AuditableBaseEntity MMCA.Common.Domain MMCA.Common.Shared RedactableProperty AddressInvariants"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#the-entity-chain-one-capability-per-rung","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"The entity chain, one capability per rung","x":"Read the chain bottom-up. BaseEntity (MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/BaseEntity.cs:14) is almost nothing: a single required init identifier of the per-entity…","i":"AuditableAggregateRootEntity AuditSaveChangesInterceptor ChangeTracker.Entries AuditableBaseEntity GetChildOrNotFound RemoveDomainEvents ClearDomainEvents IAuditableEntity ValidateSetItems TIdentifierType AddDomainEvent entry.Property"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#two-opt-in-markers-beside-the-chain","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Two opt-in markers beside the chain","x":"Not every cross-cutting capability belongs on the inheritance chain, because not every entity should pay for it. ITenantEntity…","i":"AuditTrailSaveChangesInterceptor TenantSaveChangesInterceptor entity.HasQueryFilter ApplyTenantFilters IAuditableEntity TenantFilterName AddMultiTenancy AuditTrailEntry IAuditedEntity AddAuditTrail configuration ITenantEntity"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#how-a-domain-event-leaves-an-aggregate","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"How a domain event leaves an aggregate","x":"The runtime flow ties this group to the events/outbox group. A command handler loads an aggregate, calls a business method, and that method calls AddDomainEvent(...); the event…","i":"DomainEventSaveChangesInterceptor context.ChangeTracker.Entries RemoveDomainEvents DomainEntityState IIntegrationEvent DeferredDispatch OutboxProcessor AddDomainEvent IAggregateRoot OutboxMessage IDomainEvent Unchanged"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#value-objects-invalid-instances-cannot-exist","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Value objects, invalid instances cannot exist","x":"The second family models concepts with no identity: two Money(10, USD) are equal because their values match, not because they are the same row. ValueObject is the cheapest…","i":"EnsurePreferredCultureIsValid EnsurePreferredThemeIsValid EnsureCollectionIsNotEmpty InvalidOperationException PhoneNumberValueConverter EnsureMoneyIsNotNegative EnsureBytesAreNotEmpty EnsureStringIsNotEmpty CurrencyJsonConverter EnsureStringMaxLength PhoneNumberInvariants EnsureIdIsNotDefault"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#smart-enumerations-a-closed-set-that-can-carry-behavior","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Smart enumerations, a closed set that can carry behavior","x":"Enumeration (MMCA.Common/Source/Core/MMCA.Common.Shared/ValueObjects/Enumeration.cs:71) is the answer to a recurring shape a CLR enum handles badly: a closed set of named members…","i":"ValueObjectsAreImmutableSealedInShared JsonSerializerOptions.Converters EnumerationJsonConverterFactory Enumeration.UnknownValue Enumeration.UnknownName CurrencyJsonConverter EnumerationConverter ReadOnlyCollection FrozenDictionary JsonConverter JsonException TEnumeration"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#governance-markers-metadata-that-other-layers-act-on","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Governance markers, metadata that other layers act on","x":"The last family is tiny attributes and helpers that carry intent the rest of the stack reads reflectively. PiiAttribute…","i":"AuditTrailSaveChangesInterceptor CultureInfo.InvariantCulture IdValueGeneratedAttribute PiiRedactor.RedactedToken EncryptedStringConverter PiiConventionTestsBase ConcurrentDictionary EntityTypeExtensions GetCustomAttribute IsIdValueGenerated MMCA.Common.Domain PiiConventionTests"},{"u":"/docs/onboarding/group-02-domain-building-blocks.html#where-this-group-sits","d":"2. Domain Building Blocks (Entities, Value Objects, Aggregates)","k":"Onboarding Guide","t":"Where this group sits","x":"Everything above is consumed by the layers that follow: every module entity (for example the Conference domain, Engagement, and Identity modules) derives from one of the three…","i":"EnumerationJsonConverterFactory.CreateConverter PhoneNumberInvariants.EnsurePhoneNumberIsValid AddressInvariants.EnsureAddressLine1IsValid MMCA.Common.Domain.Interfaces.IAnonymizable AddressInvariants.AddressLine1MaxLength EntityTypeExtensions.IsIdValueGenerated AddressInvariants.EnsureAddressIsValid EventInvariants.EnsureDateRangeIsValid ValueObjectsAreImmutableSealedInShared EntityTypeBuilderExtensions.OwnsMoney EntitiesWithPiiImplementAnonymizable EmailInvariants.EnsureEmailIsValid"},{"u":"/docs/onboarding/group-03-querying-specifications.html","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","x":"What this group covers. Every read in MMCA.Common and ADC (\"list the published events\", \"get session 42\", \"the speakers in Atlanta, page 3, sorted by name, with only the name and…","i":"QuerySpecification Expression IQueryable TEntity OFFSET SELECT ORDER WHERE bool Func name bio"},{"u":"/docs/onboarding/group-03-querying-specifications.html#the-specification-pattern-the-trusted-predicate","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"The Specification pattern, the trusted predicate","x":"ISpecification (MMCA.Common/Source/Core/MMCA.Common.Domain/Interfaces/ISpecification.cs:12) exposes two faces of one rule: a Criteria expression tree that EF Core translates to…","i":"PublicSessionStatusSpecification.StatusCriteria GetPublicSessionFilterHandler PublishedEventSpecification CrossSourceSpecification OwnedByUserSpecification SpecificationExtensions SpecificationComposer dependent.ForeignKey InvocationExpression Enumerable.Contains InlineSpecification s.Event.IsPublished"},{"u":"/docs/onboarding/group-03-querying-specifications.html#queryspecification-a-whole-read-in-one-object","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"QuerySpecification, a whole read in one object","x":"A plain specification is only a predicate, which leaves includes, ordering, paging, and tracking to be threaded through every layer as loose arguments. QuerySpecification…","i":"IgnoreQueryFilters QuerySpecification EFReadRepository LambdaExpression OrderExpression TIdentifierType WithSoftDeleted specification Specification BaseQueryFor IncludePaths WithTracking"},{"u":"/docs/onboarding/group-03-querying-specifications.html#dynamic-filtering-one-strategy-per-clr-type","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"Dynamic filtering, one Strategy per CLR type","x":"User filters arrive as a Dictionary , property name to operator key plus raw string value, parsed from the query string by QueryFilterModelBinder at the API edge, which caps a…","i":"Filter.Operator.NotSupported QueryParameterizationTests Filter.Property.NotFound Filter.Type.NotSupported datetimefilterstrategy QueryFilterModelBinder ResolveFilterValueType decimalfilterstrategy Filter.Value.Invalid StringFilterStrategy ResolvePropertyInfo boolfilterstrategy"},{"u":"/docs/onboarding/group-03-querying-specifications.html#sorting-sparse-fieldsets-and-paging-arithmetic","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"Sorting, sparse fieldsets, and paging arithmetic","x":"QueryFieldService (MMCA.Common/Source/Core/MMCA.Common.Application/Services/QueryFieldService.cs:16) owns the rest of read shaping. ApplySorting (QueryFieldService.cs:155)…","i":"PropertyInfo.GetValue ValidateSortDirection ApplyFieldSelection ShapeCollectionData GetShapedAccessors Expression.Lambda QueryFieldService PagingMath.Clamp PropertyAccessor MaxCacheEntries ExpandoObject ApplySorting"},{"u":"/docs/onboarding/group-03-querying-specifications.html#the-pipeline-two-entity-paths-plus-projection-pushdown","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"The pipeline: two entity paths plus projection pushdown","x":"IEntityQueryPipeline (MMCA.Common/Source/Core/MMCA.Common.Application/Services/Query/IEntityQueryPipeline.cs:10) is the execution contract, implemented by the sealed…","i":"ApplyIncludesCriteriaAndFilters inavigationmetadataprovider NavigationMetadataProvider MaxUnboundedResultLimit NavigationPropertyInfo CountUnpaginatedAsync EntityQueryParameters ExecuteProjectedAsync IEntityQueryPipeline INavigationPopulator entityquerypipeline IEntityDTOProjector"},{"u":"/docs/onboarding/group-03-querying-specifications.html#the-query-service-the-public-face","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"The query service, the public face","x":"IEntityQueryService (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/IEntityQueryService.cs:19) and its concrete EntityQueryService…","i":"SpeakerEntityQueryService BuildPaginationMetadata MaxUnboundedResultLimit TryGetByIdFastPathAsync DTOToEntityPropertyMap TryGetFastPathIncludes EntityQueryParameters ExecuteProjectedAsync PagedCollectionResult GetAllForLookupAsync INavigationPopulator DTOMapper.MapToDTOs"},{"u":"/docs/onboarding/group-03-querying-specifications.html#end-to-end-one-list-request","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"End to end, one list request","x":"The request reaches a read controller, EntityControllerBase (Group 12), which resolves MaxPageSize per request from IApplicationSettings, falling back to 500 when unset…","i":"IEntityQueryService.GetAllAsync EntityQueryParameters PagedCollectionResult EntityControllerBase IApplicationSettings EntityQueryPipeline TIdentifierType MaxPageSize PagingMath TEntityDTO requested TEntity"},{"u":"/docs/onboarding/group-03-querying-specifications.html#also-filed-here-the-best-effort-side-effect-helper","d":"3. Querying: Specifications, Filtering & the Entity Query Service","k":"Onboarding Guide","t":"Also filed here: the best-effort side-effect helper","x":"Three types in this group are not part of the read path at all; they are co-located in MMCA.Common.Application/Services and are grouped by that folder. BestEffort…","i":"Filtering.DynamicQueryConfig.Parameterized IEntityQueryPipeline.ExecuteProjectedAsync MMCA.Common.Application.Services.Filtering SpecificationsDoNotNavigateToOtherEntities ArgumentException.ThrowIfNullOrWhiteSpace QueryFilterService.ResolveFilterValueType Microsoft.Extensions.DependencyInjection NavigationMetadataProvider.BuildIncludes CrossSourceSpecification.BuildCriteria MMCA.Common.Application.Services.Query MMCA.Common.Application.Specifications QueryFieldService.ApplyFieldSelection"},{"u":"/docs/onboarding/group-04-events-outbox.html","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","x":"What this chapter covers. This group is the codebase's event spine: how an aggregate says \"something happened\", how that fact is persisted so it cannot be lost, and how it…"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-two-kinds-of-event","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The two kinds of event","x":"Everything starts with two marker interfaces in the Domain layer. IDomainEvent is the base contract: a DateOccurred timestamp (when the business action happened, not when it was…","i":"BaseIntegrationEvent EntityChangedEvent DomainEntityState IIntegrationEvent BaseDomainEvent TIdentifierType Infrastructure UserRegistered SchemaVersion Architecture DateOccurred IDomainEvent"},{"u":"/docs/onboarding/group-04-events-outbox.html#raising-and-capturing-where-the-outbox-is-written","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Raising and capturing: where the outbox is written","x":"Aggregates raise events by calling AddDomainEvent() (see AuditableAggregateRootEntity in G02), which simply buffers them on the entity. Nothing is dispatched yet; the events ride…","i":"DomainEventSaveChangesInterceptor OutboxMessage.FromDomainEvent AuditableAggregateRootEntity TIdentifierType AddDomainEvent OutboxMessages OutboxMessage SavingChanges Architecture DbContext Rubric Data"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-routing-split-local-events-dispatch-in-process-integration-events-wait-for-the-bus","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The routing split: local events dispatch in-process, integration events wait for the bus","x":"Here is the detail that most people get wrong, and it is the heart of the design. After the transaction commits (SavedChanges), the interceptor does not treat all captured events…","i":"IIntegrationEventHandler IDomainEventDispatcher SafeDomainEventHandler DomainEventDispatcher someIntegrationEvent IDomainEventHandler TIntegrationEvent DbContextFactory OutboxFinalizer OutboxProcessor AddDomainEvent ExecuteUpdate"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-safety-net-how-the-processor-schedules-itself","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The safety net: how the processor schedules itself","x":"The OutboxProcessor is a BackgroundService and the most intricate type in the group; most of its complexity is about not wasting work. It exists because the steps between commit…","i":"PollingIntervalSeconds ProcessingDelaySeconds BackgroundService OutboxCycleResult ComputeWaitTime OutboxProcessor OutboxSettings ExecuteUpdate IOutboxSignal SemaphoreSlim LeaseSeconds OutboxSignal"},{"u":"/docs/onboarding/group-04-events-outbox.html#failures-dead-letters-and-keeping-the-table-and-telemetry-bounded","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Failures, dead-letters, and keeping the table (and telemetry) bounded","x":"Delivery failures split into two very different outcomes, worth keeping straight. A transient failure (a handler or broker publish throwing) increments the row's RetryCount,…","i":"OutboxPollFilterProcessor outbox.dead_letter.count DeadLetterRetentionDays RetryBackoffBaseSeconds CleanupIntervalHours OutboxCleanupService MMCA.Common.Outbox Observability OutboxMetrics RetentionDays TimeProvider Operability"},{"u":"/docs/onboarding/group-04-events-outbox.html#the-pluggable-transport-in-process-versus-broker","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"The pluggable transport: in-process versus broker","x":"Here is the boundary that makes a module extractable without rewriting its handlers. Application code that wants to publish an integration event depends on IEventBus (or on the…","i":"InProcessMessageBus AddBrokerMessaging IIntegrationEvent InProcessEventBus BrokerMessageBus OutboxFinalizer OutboxProcessor BrokerEventBus Microservices Application IMessageBus IEventBus"},{"u":"/docs/onboarding/group-04-events-outbox.html#consuming-from-the-broker-the-inbox-and-the-generic-consumer","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Consuming from the broker: the inbox and the generic consumer","x":"On the receiving side of a broker hop, application code keeps writing plain IIntegrationEventHandler implementations; there is no MassTransit-specific consumer class to author…","i":"IntegrationEventConsumerExtensions RegisterIntegrationEventConsumer IBusRegistrationConfigurator IIntegrationEventHandler IntegrationEventConsumer AlreadyProcessedAsync MarkProcessedAsync DbUpdateException NoOpInboxStore EfInboxStore InboxMessage AddConsumer"},{"u":"/docs/onboarding/group-04-events-outbox.html#putting-it-together-one-events-life","d":"4. Domain & Integration Events + Outbox Dual-Dispatch","k":"Onboarding Guide","t":"Putting it together, one event's life","x":"To see the whole spine at once, follow a single integration event from a producer service to a consumer service in broker mode. (1) A command mutates an aggregate, which raises…","i":"MMCA.Common.Infrastructure.Persistence.Outbox MMCA.Common.Infrastructure.Persistence.Inbox Microsoft.Extensions.Hosting.IHostedService OutboxProcessor.ProcessPendingMessagesAsync UserSessionBookmarkCacheEvictionHandler services.AddOutputCacheEvictionHandler Microsoft.Extensions.Logging.ILogger MMCA.Common.Application.DomainEvents MMCA.Common.Domain.IntegrationEvents ApplicationDbContext.ConfigureInbox domainEventDispatcher.DispatchAsync MMCA.Common.Infrastructure.Services"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","x":"What this group covers. Every write and every read in an MMCA application is a use case: a small, single-purpose object (a command or a query) handed to a handler that does…","i":"AuthorizationCommandDecorator TransactionalCommandDecorator AuthorizationQueryDecorator FeatureGateCommandDecorator ValidatingCommandDecorator FeatureGateQueryDecorator ProfilingCommandDecorator CachingCommandDecorator LoggingCommandDecorator ProfilingQueryDecorator TimeoutCommandDecorator CachingQueryDecorator"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#the-shape-thin-handlers-fat-pipeline","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"The shape: thin handlers, fat pipeline","x":"A handler is deliberately tiny. ICommandHandler (MMCA.Common/Source/Core/MMCA.Common.Application/UseCases/ICommandHandler.cs:9) and IQueryHandler…","i":"cancellationToken CancellationToken ICommandHandler IQueryHandler HandleAsync Patterns TCommand default TResult Design Result Rubric"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#how-the-pipeline-is-assembled-scrutor-registration-versus-execution-order","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"How the pipeline is assembled (Scrutor, registration versus execution order)","x":"The wiring lives in DependencyInjection.cs (MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:21), exposed as extension(IServiceCollection services) members…","i":"DecoratorPipelineOrderTestsBase ScanModuleApplicationServices ProfilingCommandDecorator AddApplicationDecorators DependencyInjectionTests AddApplicationProfiling ProfilingQueryDecorator DependencyInjection.cs EntityQueryPipeline IServiceCollection ServiceCollection ICommandHandler"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#why-this-exact-order-and-what-each-layer-guards","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Why this exact order, and what each layer guards","x":"The nesting order is a deliberate cost-and-correctness argument, spelled out in the registration XML-doc (DependencyInjection.cs:76-98): - Feature-gating is outermost so a…","i":"TransactionCommitAmbiguousException ICacheService.RemoveByPrefixAsync Authorization.PermissionDenied IFeatureManager.IsEnabledAsync AuthorizationCommandDecorator TransactionalCommandDecorator AuthorizationQueryDecorator FeatureGateCommandDecorator OperationCanceledException ValidatingCommandDecorator CqrsMetrics.QueryDuration ExecuteInTransactionAsync"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#opt-in-by-marker-interface-pay-only-for-what-you-use","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Opt-in by marker interface, pay only for what you use","x":"The pipeline is registered for every handler, but most decorators are dormant unless the use case asks for them. The switch is a set of tiny marker / role interfaces in…","i":"MMCA.Common.Application.UseCases GetProductByIdQuery IRequiresPermission GetTicketByIdQuery ICacheInvalidating FeatureManagement GetOrderByIdQuery GetNowNextQuery IQueryCacheable ITransactional CacheDuration IFeatureGated"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#tenant-scoping-and-the-two-lock-tables","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Tenant scoping and the two lock tables","x":"Both caching decorators are multi-tenant aware, and the reason is a nice illustration of where a cross-cutting concern has to live. ICacheService is a singleton and therefore…","i":"ICacheService.GetOrCreateAsync CachingQueryDecorator KeyedSemaphoreStripe QueryCacheKeyLocks ITenantContext TenantCacheKey CacheKeyLocks ICacheService IsResolved tenantId TenantId TResult"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#two-supporting-pieces-and-a-worked-example","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Two supporting pieces, and a worked example","x":"Two small helpers make the short-circuit decorators possible. ResultFailureFactory…","i":"cqrs.authorization.denied.count AuditableAggregateRootEntity TypeInitializationException InvalidOperationException RecordAuthorizationDenied DeleteSessionCommand DeleteSpeakerCommand ResultFailureFactory DeleteEntityCommand DeleteEntityHandler cqrs.timeout.count ICacheInvalidating"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#the-other-application-layer-contracts-in-this-group","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"The other Application-layer contracts in this group","x":"Five contracts sit beside the pipeline rather than inside it. They are declared here, in the Application layer, and implemented in Infrastructure or the composition root, which…","i":"AuditTrailSaveChangesInterceptor InProcessDistributedLock IEntityRequestMapper RedisDistributedLock ICommandWithRequest IEntityDTOProjector EntityQueryService ScheduledJobRunner cancellationToken IAuditTrailReader AuditTrailReader IAsyncDisposable"},{"u":"/docs/onboarding/group-05-cqrs-pipeline.html#where-this-fits-and-the-failure-mode-contract","d":"5. CQRS: Commands, Queries & the Decorator Pipeline","k":"Onboarding Guide","t":"Where this fits, and the failure-mode contract","x":"These contracts sit in the Application layer of Clean Architecture (primer §1), above Domain and below Infrastructure and the API. The API layer (G12) resolves a closed handler…","i":"Microsoft.FeatureManagement.IFeatureManager MMCA.Common.Application.UseCases.Decorators Microsoft.Extensions.DependencyInjection CqrsMetrics.RecordAuthorizationDenied QueryCacheKeyLocks.Locks.AcquireAsync MMCA.Common.Application.Extensions MMCA.Common.Application.Interfaces ICacheService.RemoveByPrefixAsync correlationContext.CorrelationId MMCA.Common.Application.UseCases System.Diagnostics.Metrics.Meter ConferenceCategoryCreateRequest"},{"u":"/docs/onboarding/group-06-validation.html","d":"6. Validation","k":"Onboarding Guide","x":"This chapter covers the small, framework-level validation kit that MMCA.Common.Application ships so that every consuming module validates command input the same way: a set of…","i":"AddressInvariants.AddressLine1MaxLength AddressInvariants.AddressLine2MaxLength ValidationFailureExtensions.ToErrors AddValidatorsFromAssemblyContaining AddressInvariants.CountryMaxLength AddressInvariants.ZipCodeMaxLength MMCA.Common.Application.Extensions MMCA.Common.Application.Validation System.Linq.Expressions.Expression AddressInvariants.StateMaxLength AddressInvariants.CityMaxLength MMCA.Common.Shared.ValueObjects"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html","d":"7. Persistence & EF Core","k":"Onboarding Guide","x":"What this group covers. This is the framework's data-access engine: everything between a domain aggregate and a row in a database. It is the single largest group in the guide…","i":"ApplicationDbContext SQLServerDbContext IWriteRepository SaveChangesAsync CosmosDbContext IReadRepository SqliteDbContext TIdentifierType IRepository UnitOfWork TEntity"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#one-base-context-one-class-per-engine-one-instance-per-database","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"One base context, one class per engine, one instance per database","x":"ApplicationDbContext (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/ApplicationDbContext.cs:39) is an abstract primary-constructor class over EF's…","i":"Database.CreateExecutionStrategy DataSourceModelCacheKeyFactory IX_AuditTrailEntries_Entity IX_OutboxMessages_Processed IX_InboxMessages_MessageId IX_ScheduledJobs_NextRunOn PendingModelChangesWarning IX_OutboxMessages_Pending BeginTransactionAsync ChangeTracker.Entries ApplicationDbContext EnableRetryOnFailure"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#savechanges-as-an-interceptor-pipeline","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"SaveChanges as an interceptor pipeline","x":"The base context resolves its interceptors from DI in OnConfiguring (ApplicationDbContext.cs:236-261), and registration order is execution order. The audit interceptor runs…","i":"DomainEventSaveChangesInterceptor AuditSaveChangesInterceptor DiscardAbandonedCapture IDomainEventDispatcher BeginCaptureExclusion ConditionalWeakTable EndCaptureExclusion FlushDeferredAsync GetRequiredService RemoveDomainEvents CurrentSaveUserId IIntegrationEvent"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#the-tenant-boundary-read-filter-plus-write-guard","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"The tenant boundary, read filter plus write guard","x":"Multi-tenancy (ADR-073) is two independent halves that meet in this group. The read half is the named Tenant query filter the base context applies to every non-owned…","i":"TenantSaveChangesInterceptor CrossTenantWriteException InvalidOperationException TenantDataSourceTargets TenantDataSourceTarget ApplicationDbContext IgnoreQueryFilters CurrentTenantId ITenantEntity TenantContext e.TenantId SoftDelete"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#recording-what-changed-the-audit-trail","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Recording what changed, the audit trail","x":"AuditTrailSaveChangesInterceptor (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/AuditTrail/AuditTrailSaveChangesInterceptor.cs:62) is the fourth interceptor and…","i":"AuditTrailSaveChangesInterceptor AuditTrailCleanupJob AuditTrailReader AuditTrailEntry IAuditedEntity AddAuditTrail ExecuteDelete RedactedToken RetentionDays PiiAttribute PropertyName PiiRedactor"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#repositories-and-the-unit-of-work","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Repositories and the unit of work","x":"Handlers do not touch a DbContext directly. They ask a UnitOfWork (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/UnitOfWork.cs:13) for a repository. The…","i":"TransactionCommitAmbiguousException DefaultSqlServerDbContextFactory ApplicationDbContextEFFactory DefaultCosmosDbContextFactory DefaultSqliteDbContextFactory UpdatePropertySetterBuilder EFReadRepositoryDecorator ExecuteInTransactionAsync HasPendingMigrationsAsync IPhysicalDbContextFactory ChangeTracker.HasChanges PhysicalDbContextFactory"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#routing-an-entity-to-its-database","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Routing an entity to its database","x":"The heart of ADR-006 is that every entity resolves to a DataSourceKey (MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/Infrastructure/DataSourceKey.cs:15), a (Engine,…","i":"IEntityDataSourceRegistry EntityDataSourceRegistry UseDataSourceAttribute NamespaceConventions UseDatabaseAttribute IDataSourceResolver DataSourceResolver DataSourceService DataSourceKey GetModuleName DataSources DataSource"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#two-model-finalizing-conventions","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Two model-finalizing conventions","x":"The base context adds both of its conventions in ConfigureConventions (ApplicationDbContext.cs:282-297), and each exists because a cross-cutting policy above would otherwise…","i":"CrossDataSourceDegradeConvention SoftDeleteUniqueIndexConvention IndexBuilderExtensions ConfigureConventions INavigationPopulator HasSoftDeleteFilter SoftDeleteFilterSql IndexBuilder extension IsDeleted TEntity Build"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#entity-configuration-and-engine-portability","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Entity configuration and engine portability","x":"Concrete entity configurations derive from the engine-aware EntityTypeConfiguration…","i":"DefaultEntityConfigurationAssemblyProvider IEntityConfigurationAssemblyProvider IEntityTypeConfigurationSQLServer NullableEnumerationValueConverter NullablePhoneNumberValueConverter EntityTypeConfigurationSQLServer IEntityTypeConfigurationCosmos IEntityTypeConfigurationSqlite EntityTypeConfigurationCosmos EntityTypeConfigurationSqlite PushNotificationConfiguration UserNotificationConfiguration"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#encryption-seeding-design-time-and-the-shared-helpers","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Encryption, seeding, design time, and the shared helpers","x":"A handful of supporting pieces round out the EF side. EncryptedStringConverter…","i":"PaymentReconciliationService IDesignTimeDbContextFactory DesignTimeDbContextOptions IdentityModuleDbSeederBase DesignTimeDbContextHelper NullDomainEventDispatcher PeriodicBackgroundService EncryptedStringConverter EntityDataSourceRegistry ExplicitAssemblyProvider EFQueryableExecutor DataSourceResolver"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#blobs-images-and-native-push","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Blobs, images, and native push","x":"The group also carries the storage-adjacent infrastructure services that are not EF at all, each behind an Application-layer interface with a null default so a host that has not…","i":"AzureNotificationHubNativePushSender AzureNotificationHubDeviceRegistrar AzureBlobFileStorageService ImageSharpImageProcessor NullPushDeviceRegistrar NullFileStorageService IPushDeviceRegistrar NullNativePushSender IFileStorageService ImageContentSniffer NativePushPayloads INativePushSender"},{"u":"/docs/onboarding/group-07-persistence-ef-core.html#where-this-group-sits","d":"7. Persistence & EF Core","k":"Onboarding Guide","t":"Where this group sits","x":"Persistence is the concrete floor the abstract domain stands on. The entity bases and audit contracts from Group 02 are what the interceptors stamp and the query filters hide;…","i":"MMCA.Common.Application.Interfaces.Infrastructure MMCA.Common.Infrastructure.Persistence.AuditTrail MMCA.Common.Infrastructure.Persistence.DbContexts MMCA.Common.Infrastructure.Persistence.Encryption DomainEventSaveChangesInterceptor.DropDeferred EntityTypeConfiguration.ApplyEngineConventions Microsoft.Extensions.Hosting.BackgroundService AddInfrastructure_RegistersIRepositoryFactory CrossTenantWriteException.ForUnresolvedTenant ModelBuilderExtensions.ApplyAllConfigurations DangerousAcceptAnyServerCertificateValidator RelationalEventId.PendingModelChangesWarning"},{"u":"/docs/onboarding/group-08-auth.html","d":"8. Authentication & Authorization","k":"Onboarding Guide","x":"What this group covers. This is the security spine of the framework: how a caller proves who they are (authentication), how the system decides what they may do (authorization),…","i":"SessionCookieAuthenticationHandler PermissionAuthorizationHandler AuthenticationServiceBase AuthenticationValidators ClaimBasedUserIdProvider AuthorizationExtensions ILoginProtectionService IPasswordChangeableUser LoginProtectionSettings CookieSessionRefresher IAuthenticationService LoginProtectionService"},{"u":"/docs/onboarding/group-08-auth.html#tokens-one-signing-switch-two-validation-worlds","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Tokens: one signing switch, two validation worlds","x":"The framework mints two credentials on every successful login: a short-lived access token (a JWT, 15 minutes by default,…","i":"OidcDiscoveryEndpointExtensions OpenIdConnectMetadataWarmupTask GetPrincipalFromExpiredToken ExecutionAndPublication JwksEndpointExtensions RandomNumberGenerator JwtSigningAlgorithm IValidatableObject additionalClaims SigningAlgorithm PublicationOnly RsaJwksProvider"},{"u":"/docs/onboarding/group-08-auth.html#the-shared-authentication-workflow","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"The shared authentication workflow","x":"Login, registration, refresh, and revocation are not re-implemented per app. They live once in AuthenticationServiceBase…","i":"RefreshTokenRequestValidator AuthenticationServiceBase FindUntrackedByEmailAsync AuthenticationValidators OAuthCodeExchangeRequest AuthenticationResponse CancellationToken.None IAuthenticationService AuthenticationRequest AuthenticationService ChangePasswordRequest LoginRequestValidator"},{"u":"/docs/onboarding/group-08-auth.html#what-the-apps-user-aggregate-must-expose","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"What the app's User aggregate must expose","x":"The shared workflows never see an app's User class. They see four small Domain-layer contracts, each sized to one workflow, which is the [Rubric §1, SOLID] interface-segregation…","i":"GetUserPreferencesHandlerBase ChangePreferencesHandlerBase ChangePasswordHandlerBase ChangePreferencesRequest IPasswordChangeableUser UserPreferencesResponse DeleteUserHandlerBase AuditableBaseEntity RevokeRefreshToken UpdateRefreshToken UpdatePreferences IUserPreferences"},{"u":"/docs/onboarding/group-08-auth.html#passwords-and-brute-force-protection","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Passwords and brute-force protection","x":"Password material is handled by PasswordHasher (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Services/PasswordHasher.cs:12), which hashes with PBKDF2-HMAC-SHA512 at 600,000…","i":"CryptographicOperations.FixedTimeEquals ILoginProtectionService LoginProtectionSettings LoginProtectionService IDistributedCache MaxFailedAttempts MaxLockoutSeconds IPasswordHasher PasswordHasher ICacheService Email Range"},{"u":"/docs/onboarding/group-08-auth.html#reading-identity-from-claims","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Reading identity from claims","x":"Once a request is authenticated, downstream code needs the caller's identity without re-parsing the JWT. CurrentUserService…","i":"CultureInfo.InvariantCulture ClaimBasedUserIdProvider IHttpContextAccessor ICurrentUserService CurrentUserService ClaimsPrincipal IUserIdProvider AuthClaimTypes GetClaimValue Clients.User TokenService IsInRole"},{"u":"/docs/onboarding/group-08-auth.html#authorization-roles-permissions-ownership","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Authorization: roles, permissions, ownership","x":"The framework supports three overlapping authorization styles, wired together by the single AddAuthorizationPolicies() extension in AuthorizationExtensions…","i":"PermissionAuthorizationHandler AllowMissingOwnerAttribute OwnerOrAdminFilterOptions PermissionRegistryBuilder AddAuthorizationPolicies PermissionPolicyProvider AuthorizationExtensions HasPermissionAttribute AuthorizationPolicies PermissionRequirement RequireAuthenticated IPermissionRegistry"},{"u":"/docs/onboarding/group-08-auth.html#session-cookies-keeping-ssr-authenticated","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Session cookies: keeping SSR authenticated","x":"The final cluster solves a Blazor-specific problem: an interactive Blazor app keeps its access token in browser memory, but a cold server-side render (a new tab, an F5, an…","i":"CookieSessionRefreshMiddlewareExtensions SessionCookieAuthenticationExtensions SessionCookieAuthenticationHandler CookieSessionRefreshMiddleware ICookieSessionRefresher CookieSessionRefresher SessionCookieEndpoints KeyedSemaphoreStripe SessionCookieRequest SessionTokenResponse SessionTokenResult CookieTokenReader"},{"u":"/docs/onboarding/group-08-auth.html#privacy-the-data-subject-export-package","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Privacy: the data-subject export package","x":"Three members of this group belong to the privacy surface that sits beside erasure. UserDataExportDTO (MMCA.Common/Source/Core/MMCA.Common.Shared/Privacy/UserDataExportDTO.cs:15)…","i":"ExportUserDataHandlerBase DataExportControllerBase UserDataExportSectionDTO IUserDataExportSection Privacy.DataExport UserDataExportDTO PrivacyFeatures FormatVersion FeatureGate Authorize Available Subject"},{"u":"/docs/onboarding/group-08-auth.html#shared-primitives-and-adjacent-members","d":"8. Authentication & Authorization","k":"Onboarding Guide","t":"Shared primitives and adjacent members","x":"Four group members are general-purpose primitives that landed in this chapter because of how the dependency grouping fell, though one of them is now load-bearing for auth.…","i":"MMCA.Common.Application.Interfaces.Infrastructure AuthorizationExtensions.AddAuthorizationPolicies context.ActionDescriptor.EndpointMetadata.OfType Microsoft.AspNetCore.Http.IHttpContextAccessor JsonWebKeyConverter.ConvertFromRSASecurityKey SessionCookieAuthenticationHandler.SchemeName Microsoft.AspNetCore.SignalR.IUserIdProvider Microsoft.IdentityModel.Tokens.JsonWebKeySet services.AddValidatorsFromAssemblyContaining ArgumentException.ThrowIfNullOrWhiteSpace CookieTokenReader.FreshAccessTokenItemKey ICookieSessionRefresher.GetOrRefreshAsync"},{"u":"/docs/onboarding/group-09-caching.html","d":"9. Caching","k":"Onboarding Guide","x":"What this group covers. Caching in this codebase is small, deliberate, and woven into the CQRS pipeline rather than scattered across handlers. The group is eight types: one port…","i":"Microsoft.Extensions.Caching.Hybrid.HybridCache HybridCacheEntryFlags.DisableUnderlyingData StackExchange.Redis.IConnectionMultiplexer Microsoft.Extensions.Options.IOptions MMCA.Common.Application.Interfaces MMCA.Common.Infrastructure.Caching DistributedCacheServiceRedisTests connectionMultiplexer.GetServers AbsoluteExpirationRelativeToNow PublicEndpointOutputCachePolicy System.Text.Json.JsonSerializer LogPrefixEvictionNoMultiplexer"},{"u":"/docs/onboarding/group-10-notifications.html","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","x":"What this group covers. This is the notification subsystem, the machinery that turns \"an organizer wants to tell every attendee something\" into messages that actually reach…","i":"INotificationRecipientProvider NullPushNotificationSender NullLiveChannelPublisher IPushNotificationSender ILiveChannelPublisher NotificationModule DevicesController INativePushSender UserNotification NotificationHub SmtpEmailSender IEmailSender"},{"u":"/docs/onboarding/group-10-notifications.html#the-layering-and-why-the-pieces-sit-where-they-do","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The layering, and why the pieces sit where they do","x":"The dependency flow of the group mirrors the framework's Clean Architecture story ([Rubric §3, Clean Architecture]). The Domain layer holds the two aggregates, PushNotification…","i":"NullNotificationRecipientProvider Notification.PushNotifications SignalRPushNotificationSender SendPushNotificationRequest SignalRLiveChannelPublisher NullPushNotificationSender PushNotificationInvariants DeviceInstallationRequest NullLiveChannelPublisher NotificationsController PushNotificationCreated PushNotificationStatus"},{"u":"/docs/onboarding/group-10-notifications.html#the-broadcast-send-flow-end-to-end","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The broadcast send flow, end to end","x":"Sending a notification is a command-side vertical slice ([Rubric §5, Vertical Slice], [Rubric §6, CQRS & Event-Driven]). An organizer POSTs to NotificationsController, which is…","i":"NotificationFeatures.PushNotifications AttendeeNotificationRecipientProvider AddNotificationApplicationServices NullNotificationRecipientProvider INotificationRecipientProvider PushNotification.NoRecipients unitOfWork.GetReadRepository SendPushNotificationCommand SendPushNotificationHandler PushNotificationDTOMapper IPushNotificationSender NotificationsController"},{"u":"/docs/onboarding/group-10-notifications.html#the-inbox-side","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The inbox side","x":"Reading and acknowledging notifications is the query/command counterpart, served by InboxController under the same feature gate and [Authorize(RequireAuthenticated)], so any user…","i":"GetUnreadNotificationCountQuery MarkAllNotificationsReadCommand MarkNotificationReadCommand MarkNotificationReadHandler ICurrentUserService.UserId GetMyNotificationsHandler UserNotification.NotFound GetMyNotificationsQuery RequireAuthenticated PushNotification UserNotification InboxController"},{"u":"/docs/onboarding/group-10-notifications.html#the-signalr-transport-and-how-it-survives-extraction","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The SignalR transport, and how it survives extraction","x":"NotificationHub is intentionally thin (MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Hubs/NotificationHub.cs:16-17): it is [Authorize]d, and beyond ASP.NET's built-in…","i":"LiveChannelPublisherGrpcAdapter services__notification__grpc__0 SignalRPushNotificationSender SignalRLiveChannelPublisher NullLiveChannelPublisher LiveChannelGrpcService ILiveChannelPublisher AddPushNotifications RequireAuthorization _grpc.notification MapNotificationHub NotificationHub"},{"u":"/docs/onboarding/group-10-notifications.html#the-module-host-native-device-registration-and-the-privacy-export","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"The module host, native-device registration, and the privacy export","x":"On the ADC side the whole capability is packaged by NotificationModule (MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.API/NotificationModule.cs:15), an IModule that…","i":"UserNotificationExportServiceGrpcAdapter DisabledUserNotificationExportService UserNotificationExportGrpcService IUserNotificationExportService UserNotificationExportItemDTO UserNotificationExportService currentUserService.UserId DeviceInstallationRequest AddNotificationModule IPushDeviceRegistrar RequiresDependencies DependencyInjection"},{"u":"/docs/onboarding/group-10-notifications.html#where-this-group-sits","d":"10. Notifications (Push + In-App Inbox + Email)","k":"Onboarding Guide","t":"Where this group sits","x":"Upstream, this group depends on the domain building blocks of Group 02 (both aggregates derive from AuditableAggregateRootEntity ), the Result pattern of Group 01, the CQRS…","i":"LiveChannelPushService.LiveChannelPushServiceBase MMCA.Common.Application.Interfaces.Infrastructure MMCA.ADC.Notification.Shared.UserNotifications attendeeQueryService.GetAttendeeUserIdsAsync services.AddNotificationApplicationServices PushNotificationDTOProjection.ProjectToDTO PushNotificationProjectionTranslationTests MMCA.Common.API.Controllers.Notifications NotificationHub.ReceiveNotificationMethod PushNotificationInvariants.TitleMaxLength Microsoft.Extensions.DependencyInjection PushNotificationInvariants.BodyMaxLength"},{"u":"/docs/onboarding/group-11-navigation-populators.html","d":"11. Navigation Metadata & Populators (EF-decoupled eager loading)","k":"Onboarding Guide","x":"EF Core gives you .Include() for eager loading, and for a single SQL Server database that is the right tool. But this codebase is a database-per-service modular monolith…","i":"navigationMetadata.UnsupportedIncludes.Count MMCA.Common.Application.Services.Navigation NavigationLoader.LoadChildrenPropertyAsync NavigationMetadataProvider.BuildIncludes NavigationMetadata.UnsupportedIncludes IDataSourceService.HaveIncludeSupport NavigationLoader.LoadFKPropertyAsync INavigationPopulator.PopulateAsync MMCA.Common.Application.Interfaces DeclarativeNavigationPopulator.cs CrossDataSourceDegradeConvention EntityQueryPipeline.ExecuteAsync"},{"u":"/docs/onboarding/group-12-api-hosting-mapping.html","d":"12. API Hosting, Middleware, Idempotency & DTO/Contract Mapping","k":"Onboarding Guide","x":"What this group covers. This is the ASP.NET Core edge of the framework: the layer that turns an HTTP request into a domain call and turns a Result back into an HTTP response.…","i":"Microsoft.Extensions.Localization.LocalizedString Microsoft.AspNetCore.Http.IProblemDetailsService Microsoft.EntityFrameworkCore.DbUpdateException Microsoft.AspNetCore.Http.IHttpContextAccessor Microsoft.IdentityModel.Tokens.JsonWebKeySet System.Threading.RateLimiting.RateLimitLease IDbContextFactory.HasPendingMigrationsAsync AuthorizationPolicies.RequireAuthenticated Microsoft.AspNetCore.Authentication.Google StackExchange.Redis.IConnectionMultiplexer ArgumentException.ThrowIfNullOrWhiteSpace System.Threading.RateLimiting.RateLimiter"},{"u":"/docs/onboarding/group-13-grpc-contracts.html","d":"13. gRPC & Inter-Service Contracts","k":"Onboarding Guide","x":"What this chapter is about. Once the ADC modules stopped sharing a process and became four separate service hosts (Identity, Conference, Engagement, Notification), the in-process…","i":"Microsoft.AspNetCore.Http.IHttpContextAccessor ArgumentException.ThrowIfNullOrWhiteSpace Microsoft.Extensions.DependencyInjection ErrorHttpMapping.ErrorTypeToStatusCode AddConferenceSessionValidationClient Microsoft.Extensions.Http.Resilience Microsoft.Extensions.Logging.ILogger ResultGrpcExtensions.ThrowIfFailure ResultGrpcExtensions.ToRpcException Grpc.Core.Interceptors.Interceptor ArgumentNullException.ThrowIfNull ISessionBookmarkValidationService"},{"u":"/docs/onboarding/group-14-module-system-composition.html","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","x":"What this chapter covers. This is the wiring layer, the code that turns a pile of layered assemblies into a running host. It answers three questions a new host author asks: how…","i":"ConnectionStringSettings InProcessDistributedLock PushNotificationSettings UseDataSourceAttribute RedisDistributedLock UseDatabaseAttribute ApplicationSettings DataSourcesSettings DependencyInjection FileStorageSettings PersistenceSettings AuditTrailSettings"},{"u":"/docs/onboarding/group-14-module-system-composition.html#the-module-contract-and-the-boundary-it-creates","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"The module contract and the boundary it creates","x":"A module is the unit of cohesion above a feature slice: Conference, Engagement, Identity, Notification. Each one implements IModule…","i":"DisabledSessionBookmarkValidationService DisabledEventLiveValidationService GetSessionBookmarkCountHandler IBookmarkCountService RegisterDisabledStubs RequiresDependencies AddConferenceModule applicationSettings ConferenceModule moduleEnabled Dependencies Register"},{"u":"/docs/onboarding/group-14-module-system-composition.html#discovery-and-kahn-ordered-registration","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Discovery and Kahn-ordered registration","x":"ModuleLoader (MMCA.Common/Source/Core/MMCA.Common.Application/Modules/ModuleLoader.cs:15) is the engine. Its DiscoverAndRegister comes in two overloads: the short one…","i":"AppDomain.CurrentDomain.GetAssemblies ModulesSettings.IsModuleEnabled ValidateModuleDependencies ValidateRemoteDependencies Activator.CreateInstance IModuleSeeder.SeedAsync RegisterDisabledStubs RegisterEnabledModule RequiresDependencies DisabledModuleNames DiscoverAndRegister RemoteDependencies"},{"u":"/docs/onboarding/group-14-module-system-composition.html#the-two-composition-roots-and-the-ordering-they-enforce","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"The two composition roots and the ordering they enforce","x":"Service registration itself lives in two static DependencyInjection classes, each using a C extension(IServiceCollection services) block (see primer §4 for the extension(T)…","i":"ScanModuleApplicationServices FeatureGateCommandDecorator INavigationMetadataProvider AddNativePushNotifications AddApplicationDecorators InProcessDistributedLock AddApplicationProfiling AddAzureBlobFileStorage CommandRequestValidator LoggingCommandDecorator IConnectionMultiplexer IDomainEventDispatcher"},{"u":"/docs/onboarding/group-14-module-system-composition.html#opt-in-platform-features-are-composed-the-same-way","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Opt-in platform features are composed the same way","x":"Four newer capabilities are registered beside the roots rather than inside them, and they share one discipline: registering a feature is not the same as turning it on.…","i":"AuditTrailSaveChangesInterceptor TenantSaveChangesInterceptor AddUserDataExportSection TenancySettingsValidator IUserDataExportSection MMCA.Common.Scheduler AuditTrailEntryDTO AuditTrailSettings ScheduledJobRunner AddInfrastructure BackgroundService ScheduledJobEntry"},{"u":"/docs/onboarding/group-14-module-system-composition.html#assembly-anchors","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Assembly anchors","x":"Several pieces of machinery need a Type whose Assembly identifies a layer: Scrutor's FromAssemblyOf () scans, FluentValidation's AddValidatorsFromAssemblyContaining (), and…","i":"AddValidatorsFromAssemblyContaining AddInfrastructure AssemblyReference AddApplication ClassReference FromAssemblyOf AssemblyName Assembly static class Type"},{"u":"/docs/onboarding/group-14-module-system-composition.html#configuration-binding-the-settings-family","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Configuration binding, the Settings family","x":"Everything a host operator tunes arrives as a strongly-typed settings object bound from an appsettings.json section, each carrying a static readonly string SectionName so the…","i":"TenantDataSourceOverrideSettings EffectiveExcludedPathPrefixes ScheduledJobOverrideSettings IValidatableObject.Validate IConnectionStringSettings IPushNotificationSettings SQLServerConnectionString ConnectionStringSettings EffectiveResolutionOrder PushNotificationSettings TenancySettingsValidator TenantResolutionStrategy"},{"u":"/docs/onboarding/group-14-module-system-composition.html#the-two-routing-attributes","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"The two routing attributes","x":"Two attributes, both in MMCA.Common.Infrastructure, both Inherited = true so they ride down a configuration class hierarchy, encode where an entity is stored declaratively: the…","i":"MMCA.Common.Infrastructure EntityDataSourceRegistry UseDataSourceAttribute UseDatabaseAttribute DataSourceResolver DbContextFactory DataSource Inherited Domain true"},{"u":"/docs/onboarding/group-14-module-system-composition.html#shared-user-use-case-bases-composition-in-the-other-direction","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"Shared user use-case bases: composition in the other direction","x":"The chapter's last family is composition at the handler level rather than the container level. ADC and Store each own an Identity module, and five of their account use cases had…","i":"UserOwnershipRule.CheckOwnership GetUserPreferencesHandlerBase UserDataExportSectionDefaults ChangePreferencesHandlerBase UserDataExportSectionResult ChangePasswordHandlerBase ExportUserDataHandlerBase ISoftDeletedUserValidator SoftDeletedUserValidator GetUserPreferencesQuery IUserDataExportSection DeleteUserHandlerBase"},{"u":"/docs/onboarding/group-14-module-system-composition.html#end-to-end-one-hosts-boot","d":"14. Module System, Composition & Configuration","k":"Onboarding Guide","t":"End-to-end: one host's boot","x":"Reading MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs top to bottom shows the whole chapter cooperating. The host binds and validates ApplicationSettings and…","i":"MMCA.Common.Application.Interfaces.Infrastructure MMCA.Common.Application.Users.UseCases.DeleteUser UserDataExportSectionDefaults.UnavailableReason TenancySettingsValidator.ConnectionStringFor DefaultEntityConfigurationAssemblyProvider ArgumentException.ThrowIfNullOrWhiteSpace InProcessDistributedLock.TryAcquireAsync Microsoft.Extensions.DependencyInjection ScheduledJobRunner.ResolveCronExpression UserDataExportSectionResult.Unavailable UserUseCaseLog.ExportSectionUnavailable DbContextFactory.ResolveTenantOverride"},{"u":"/docs/onboarding/group-15-common-ui-framework.html","d":"15. Common UI Framework (MudBlazor components, theme, base pages)","k":"Onboarding Guide","x":"What this chapter covers. MMCA.Common.UI is the Blazor presentation package, and it is one of the two layers (with Grpc) allowed to reference Shared only (see primer §1). It…","i":"Microsoft.AspNetCore.Components.NavigationManager Microsoft.Extensions.Configuration.IConfiguration Microsoft.AspNetCore.Builder.IApplicationBuilder Microsoft.AspNetCore.Components.DynamicComponent MauiBackNavigationBridge.HandleBackPressedAsync Microsoft.AspNetCore.WebUtilities.QueryHelpers WasmTokenStorageService.GetAccessTokenAsync HttpResilienceDefaults.TotalRequestTimeout ArgumentException.ThrowIfNullOrWhiteSpace CultureInfo.DefaultThreadCurrentUICulture ITokenStorageService.GetAccessTokenAsync Microsoft.Extensions.DependencyInjection"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","x":"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…","i":"MMCA.Common.Aspire.Gateway MMCA.Common.Aspire.Hosting AddServiceDefaults MMCA.Common.Aspire MMCA.Common.Shared MMCA.ADC.AppHost Aspire.Hosting dotnet run"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#the-orchestrator-declaring-the-resource-graph","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"The orchestrator: declaring the resource graph","x":"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.…","i":"LoginProtection__MaxRegistrationsPerIpPerHour ConnectionStrings__SQLServerConnectionString Authentication__JwtBearer__Authority WithE2eRegistrationThrottleLift E2E_LIFT_REGISTRATION_THROTTLE E2eRegistrationsPerIpPerHour __SQLServerConnectionString MMCA.Common.Aspire.Hosting DefaultBrokerResourceName E2E_JWT_PRIVATE_KEY_PEM WithSQLServerDataSource E2E_JWT_PUBLIC_KEY_PEM"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#startup-ordering-and-the-grpc-deadlock-avoidance-trick","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Startup ordering and the gRPC deadlock-avoidance trick","x":"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,…","i":"ISessionBookmarkValidationService IBookmarkCountService AddTypedGrpcClient WaitFor"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#the-service-baseline-addservicedefaults","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"The service baseline: AddServiceDefaults()","x":"Every running host calls one method first in Program.cs: AddServiceDefaults() from the single framework-grade Extensions in MMCA.Common.Aspire (Level 2,…","i":"EnableMultipleHttp2Connections ConfigureHttpClientDefaults AddDefaultHealthChecks ConfigureOpenTelemetry AddServiceDiscovery AddServiceDefaults AddWarmupReadiness MMCA.Common.Aspire SocketsHttpHandler HttpClient Program.cs TBuilder"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#one-source-of-truth-for-resilience-numbers","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"One source of truth for resilience numbers","x":"The numbers that pipeline uses are not literals in the Aspire package. They live in HttpResilienceDefaults (Level 0,…","i":"MMCA.Common.Infrastructure MMCA.Common.Grpc Continuity properties Resilience RetryCount including Business Concerns lifetime sampling attempt"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#listeners-and-probes-one-kestrel-profile-per-host-shape","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Listeners and probes: one Kestrel profile per host shape","x":"Before a service can answer a health probe it has to be listening on a protocol the prober speaks, and that is not free when the same port serves inbound gRPC.…","i":"redeclareCleartextEndpoint ASPNETCORE_HTTP_PORTS HttpProtocols.Http2 MapDefaultEndpoints BuildListenerPlan HTTP_1_1_REQUIRED Http1AndHttp2 Deployment Protocols deployed profiles httpGet"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#health-checks-liveness-readiness-and-the-optional-tag","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Health checks: liveness, readiness, and the \"optional\" tag","x":"MapDefaultEndpoints() (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:336) exposes the three-probe surface the platform reads: /health (every check, for humans and…","i":"AddInfrastructureHealthChecks AddDefaultHealthChecks MapDefaultEndpoints requireSqlServer Observability Operability Deployment optional Optional DevOps Rubric Ready"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#telemetry-what-gets-exported-and-what-it-costs","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Telemetry: what gets exported, and what it costs","x":"ConfigureOpenTelemetry() (MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:121) wires logging with formatted messages and scopes (:123-127), metrics, and tracing. It…","i":"APPLICATIONINSIGHTS_CONNECTION_STRING ActivityTraceFlags.Recorded OTEL_EXPORTER_OTLP_ENDPOINT AddOpenTelemetryExporters IsInstrumentationDisabled TraceIdRatioBasedSampler MMCA.Common.Idempotency MMCA.Common.OutputCache ConfigureOpenTelemetry MMCA.Common.BestEffort TryGetTraceSampleRatio MMCA.Common.Scheduler"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#warm-up-defeating-aca-cold-start","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Warm-up: defeating ACA cold-start","x":"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…","i":"RequireSuccessStatusCode HealthCheckTags.Ready WebApplicationFactory Interlocked.Exchange RequestVersionPolicy AddServiceDefaults AddWarmupReadiness ApplicationStarted IHttpClientFactory BackgroundService ResolveWarmupPort WithJwksDiscovery"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#configuration-secrets-the-vault-as-one-more-configuration-source","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Configuration secrets: the vault as one more configuration source","x":"Connection strings, RSA signing keys and OAuth client secrets have to reach the process somehow, and KeyVaultConfigurationExtensions (Level 0,…","i":"AddCommonDataProtection DefaultAzureCredential builder.Configuration ConfigurationManager AddServiceDefaults IConfiguration Deployment Security answer DevOps Rubric the"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#security-headers-cors-and-the-shared-key-ring-at-the-host-edge","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"Security headers, CORS, and the shared key ring at the host edge","x":"The next boundary in this group hardens the request and response edge. The shared response-header middleware is defined entirely in…","i":"AddCommonSecurityHeaders UseCommonSecurityHeaders AddCommonDataProtection DefaultAzureCredential AddCommonGatewayCors AddCommonBlazorCsp PermissionsPolicy MMCA.Common.API TryAddSingleton ReferrerPolicy AddCommonCors FrameOptions"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#the-gateway-edge-kit-correlation-rate-limiting-downstream-readiness","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"The gateway edge kit: correlation, rate limiting, downstream readiness","x":"A YARP gateway is the one process every client request passes through, and it is also the one host that has no application container: no DbContext, no module loader, no…","i":"AddServiceDiscoveryDestinationResolver AddGatewayDownstreamHealthChecks Connection.RemoteIpAddress MMCA.Common.Aspire.Gateway Validator.ValidateObject HttpResponse.OnStarting ValidateDataAnnotations AddGatewayRateLimiting GlobalConcurrencyLimit UseGatewayRateLimiting ConfigureClusterAsync RequestVersionOrLower"},{"u":"/docs/onboarding/group-16-aspire-orchestration.html#how-it-all-fits-at-runtime","d":"16. Aspire Orchestration & Service Defaults","k":"Onboarding Guide","t":"How it all fits at runtime","x":"Putting the pieces in sequence: the AppHost declares the graph and injects per-service env vars (WithSQLServerDataSource, WithBroker, WithJwksDiscovery, the E2E helpers, and the…","i":"Azure.Extensions.AspNetCore.Configuration.Secrets Azure.Extensions.AspNetCore.DataProtection.Blobs identityService.WithE2eRegistrationThrottleLift AddGatewayDownstreamHealthChecks_IsIdempotent LoginProtection__MaxRegistrationsPerIpPerHour ConnectionStrings__SQLServerConnectionString database.Resource.ConnectionStringExpression Readiness_IncludesADownstreamCheckPerService ResilienceCircuitBreakerFaultInjectionTests WarmupReadinessHealthCheck.CheckHealthAsync HttpKeepAlivePingPolicy.WithActiveRequests cancellationToken.IsCancellationRequested"},{"u":"/docs/onboarding/group-17-conference-domain.html","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","x":"What this chapter covers. This is the heart of the Atlanta Developers Conference application, the Conference bounded context, the largest and richest domain in MMCA.ADC. It…","i":"AuditableAggregateRootEntity MMCA.ADC.Conference.Shared IdValueGeneratedAttribute INavigationPopulator EntityChangedEvent DomainEntityState TIdentifierType IAuditedEntity IModule TEntity Design Result"},{"u":"/docs/onboarding/group-17-conference-domain.html#two-packages-one-bounded-context","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Two packages, one bounded context","x":"The Conference context spans two of the module's projects, and the split is deliberate Clean Architecture ([Rubric §3, Clean Architecture]). MMCA.ADC.Conference.Domain holds the…","i":"ISessionBookmarkValidationService IEventLiveValidationService MMCA.ADC.Conference.Domain MMCA.ADC.Conference.Shared SessionFeedbackSubmitted SpeakerUnlinkedFromUser EventFeedbackSubmitted SpeakerLinkedToUser MMCA.Common.Domain AssemblyReference ClassReference Architecture"},{"u":"/docs/onboarding/group-17-conference-domain.html#seven-aggregates-and-their-ownership-boundaries","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Seven aggregates and their ownership boundaries","x":"An aggregate is a root entity plus the children it exclusively owns; invariants are enforced inside the boundary, and references across aggregates are by ID, never by object…","i":"AuditableAggregateRootEntity RecordSessionizeRefresh SessionQuestionAnswer SpeakerQuestionAnswer EventQuestionAnswer SessionCategoryItem SpeakerCategoryItem IDENTITY_INSERT TIdentifierType IAuditedEntity QuestionEntity QuestionSource"},{"u":"/docs/onboarding/group-17-conference-domain.html#the-aggregate-shape-taught-once","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"The aggregate shape, taught once","x":"Open any of the roots and you will see the same skeleton; this repetition is the point, and it is what makes the per-type sections that follow read quickly. The shape, using…","i":"Session.SessionQuestionAnswers Event.EventQuestionAnswers IReadOnlyCollection RestoreEventSpeaker isIdValueGenerated _rooms.AsReadOnly Result.Combine Architecture IsCollection base.Delete Performance RestoreRoom"},{"u":"/docs/onboarding/group-17-conference-domain.html#invariants-business-rules-as-testable-units","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Invariants, business rules as testable units","x":"Each aggregate has a co-located static invariant class, EventInvariants (MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Domain/Events/EventInvariants.cs:10),…","i":"System.Net.Mail.MailAddress CategoryInvariants QuestionInvariants SessionInvariants SpeakerInvariants SponsorInvariants CommonInvariants EventInvariants SessionStatuses Result.Combine Decline_Queue Accept_Queue"},{"u":"/docs/onboarding/group-17-conference-domain.html#domain-events-and-the-outbox-spine","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Domain events and the outbox spine","x":"Every state-changing method raises a domain event through the inherited AddDomainEvent(...). The events come in two shapes. The aggregate-level ones, EventChanged,…","i":"SessionCategoryItemChanged SpeakerCategoryItemChanged SessionSpeakerChanged PreviousLinkedUserId CategoryItemChanged EventSpeakerChanged EntityChangedEvent DomainEntityState SaveChangesAsync CategoryChanged QuestionChanged TIdentifierType"},{"u":"/docs/onboarding/group-17-conference-domain.html#the-cross-aggregate-cascade-a-pure-domain-service","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"The cross-aggregate cascade: a pure domain service","x":"One business rule cannot live inside a single aggregate: deleting an Event must also delete every Session belonging to it (BR-127) and every Sponsor sold against it, but sessions…","i":"IEventCascadeDeletionDomainService EventCascadeDeletionDomainService EventId Session Sponsor Design Rubric Event List"},{"u":"/docs/onboarding/group-17-conference-domain.html#read-models-and-the-ai-decision-support-feature","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Read models and the AI decision-support feature","x":"The largest cluster in Conference.Shared is the DTO layer, the wire contracts that decouple the API from the domain entities ([Rubric §9, API & Contract Design]; ADR-001 chose…","i":"RefreshFromSessionizeResultDTO RefreshFromSessionizeCommand SessionSelectionDashboardDTO ScoreEventSessionsResultDTO CategoryGroupDistribution Conference.Infrastructure CategoryItemDistribution SessionQuestionAnswerDTO SpeakerQuestionAnswerDTO SpeakerSessionOverlapDTO CategoryDistributionDTO ConcurrencyTokenRequest"},{"u":"/docs/onboarding/group-17-conference-domain.html#authorization-vocabulary-and-current-event-selection","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Authorization vocabulary and current-event selection","x":"Two more Shared helpers deserve a mention because they encode policy the whole module relies on. ConferencePermissions…","i":"TimeZoneInfo.ConvertTimeToUtc ConferenceReadAudience ConferencePermissions CurrentEventDefaults CurrentEventSelector ContentManagement ContentEditor HasPermission Organizer RoleNames StartDate EventDTO"},{"u":"/docs/onboarding/group-17-conference-domain.html#crossing-the-module-boundary-contracts-stubs-and-integration-events","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"Crossing the module boundary: contracts, stubs, and integration events","x":"Conference does not live alone. Three kinds of connection point join it to other modules, and all live in Conference.Shared so neither side reaches into the other's domain…","i":"DisabledSessionBookmarkValidationService DisabledEventLiveValidationService ISessionBookmarkValidationService IEventLiveValidationService QuestionModerationDefault SessionFeedbackSubmitted SpeakerUnlinkedFromUser Conference.Application EventFeedbackSubmitted BaseIntegrationEvent User.LinkedSpeakerId SpeakerLinkedToUser"},{"u":"/docs/onboarding/group-17-conference-domain.html#end-to-end-one-organizer-action","d":"17. ADC Conference - Domain Model & Module Contracts","k":"Onboarding Guide","t":"End-to-end: one organizer action","x":"To see the chapter cooperate, follow an organizer renaming a room on an event. The application handler loads the Event aggregate (with its Rooms hydrated by the navigation…","i":"CategoryInvariants.EnsureCategoryItemNameIsUnique IEventLiveValidationService.GetEventLiveInfoAsync MMCA.ADC.Conference.Domain.Questions.DomainEvents MMCA.ADC.Conference.Domain.Sessions.DomainEvents MMCA.ADC.Conference.Domain.Speakers.DomainEvents MMCA.ADC.Conference.Domain.Sponsors.DomainEvents IUserSpeakerLinkService.ClearLinkedSpeakerAsync MMCA.ADC.Conference.Domain.Events.DomainEvents SessionInvariants.EnsureAnswerValueIsValid SpeakerInvariants.EnsureAnswerValueIsValid CurrentEventSelector.SelectCurrentOrNext DisabledSessionBookmarkValidationService"},{"u":"/docs/onboarding/group-18-conference-application.html","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","x":"What this chapter covers. This is the application layer of the Conference module, the largest single application assembly in the codebase (this group covers 251 distinct types).…","i":"MMCA.Common.Application ics"},{"u":"/docs/onboarding/group-18-conference-application.html#the-vertical-slice-anatomy-of-a-use-case","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"The vertical-slice anatomy of a use case","x":"Open any feature folder under Sessions/UseCases/, Events/UseCases/, Speakers/UseCases/, Sponsors/UseCases/, Categories/UseCases/, or Questions/UseCases/ and you will find the…","i":"ValidateRoomAssignmentAsync BuildOverlapPredicate EventQuestionAnswers UnprocessableEntity EventSpeakers int.MinValue HandleAsync s.StartsAt DbContext s.EndsAt startsAt Session"},{"u":"/docs/onboarding/group-18-conference-application.html#manual-mapping-validation-rule-fragments-and-authorization-specifications","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Manual mapping, validation rule fragments, and authorization specifications","x":"Three sibling families recur across every aggregate. DTO mappers (SessionDTOMapper, EventDTOMapper, SpeakerDTOMapper, SponsorDTOMapper, RoomDTOMapper, CategoryItemDTOMapper, and…","i":"TimeZoneInfo.FindSystemTimeZoneById s.Event.IsPublished AbstractValidator GetProjectedAsync GetReadRepository Session.EventId SessionSpeaker e.IsPublished EventSpeaker Expression IsEligible StartDate"},{"u":"/docs/onboarding/group-18-conference-application.html#query-services-navigation-populators-and-the-composition-root","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Query services, navigation populators, and the composition root","x":"Read paths do not get bespoke handlers for the common cases; they go through the framework's generic IEntityQueryService , which supplies filtering, sorting, paging, and field…","i":"ScanModuleApplicationServices IServiceCollection ClassReference extension FirstName FullName LastName Question Sponsor"},{"u":"/docs/onboarding/group-18-conference-application.html#event-driven-reactions-domain-and-integration-handlers","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Event-driven reactions: domain and integration handlers","x":"The application layer is also where the module reacts to events. Domain event handlers implement IDomainEventHandler and run in-process after the aggregate's SaveChangesAsync.…","i":"EnsureNotServiceSession SpeakerUnlinkedFromUser EnsureStatusIsEligible User.LinkedSpeakerId SpeakerLinkedToUser GetLiveWindowUtc SaveChangesAsync SessionChanged UserRegistered LogAndRethrow IEventBus Deleted"},{"u":"/docs/onboarding/group-18-conference-application.html#attendee-facing-read-models-calendar-export-and-nownext","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Attendee-facing read models: calendar export and Now/Next","x":"A small cluster of queries serves the public schedule surfaces without going through the generic query service, because their output is not a DTO list. ExportEventCalendarHandler…","i":"CalendarExportMapper.IsExportable DateTimeOffset.UtcNow GetNowNextHandler GetLiveWindowUtc Error.NotFound IsExportable TimeProvider DTSTAMP Result string ics"},{"u":"/docs/onboarding/group-18-conference-application.html#the-sessionize-import-strategy-pattern-orchestration","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"The Sessionize import: Strategy-pattern orchestration","x":"The single most involved use case is importing a conference agenda from Sessionize.com. Sessionize returns one JSON payload covering five interdependent entity families…","i":"ThrowIfCancellationRequested TimeoutRejectedException BrokenCircuitException NotSupportedException RequestIdentityInsert HttpRequestException SaveChangesAsync JsonException TimeProvider Create Update catch"},{"u":"/docs/onboarding/group-18-conference-application.html#decision-support-ai-scoring-and-content-analytics","d":"18. ADC Conference - Application & Use Cases","k":"Onboarding Guide","t":"Decision support: AI scoring and content analytics","x":"The last cluster is session-selection decision support, analytics that help organizers triage proposals. GetSessionSelectionDashboardHandler is a composite query: it validates…","i":"MMCA.ADC.Conference.Application.Events.Sessionize MMCA.ADC.Conference.Application.Events.Validation SessionRoomScheduling.ValidateRoomAssignmentAsync currentUserService.IsPrivilegedConferenceReader eventCascadeDeletionDomainService.CascadeDelete IUserSpeakerLinkService.ClearLinkedSpeakerAsync MMCA.ADC.Conference.Application.Categories.DTOs MMCA.ADC.Engagement.Shared.UserSessionBookmarks PublicSessionStatusSpecification.StatusCriteria SessionSimilarityCalculator.CalculateSimilarity cancellationToken.ThrowIfCancellationRequested EventInvariants.OrganizerContactEmailMaxLength"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","x":"What this chapter covers. This is the adapter layer of the Conference module, the place where the engine-agnostic domain meets concrete technology. Three concerns live here: (1)…","i":"SessionScoringQueue ISessionizeService IAiScoringService Architecture DbContext Rubric Clean DbSet"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#engine-agnostic-entities-engine-chosen-by-the-config-base-class","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Engine-agnostic entities, engine chosen by the config base class","x":"The most important idea in this chapter is one the entities themselves never express: what storage engine each entity uses is decided here, not in the domain. A Conference domain…","i":"EntityTypeConfigurationSQLServer EntityDataSourceRegistry EntityTypeConfiguration DataSource.SQLServer SessionConfiguration SpeakerConfiguration SponsorConfiguration EventConfiguration TIdentifierType UseDataSource Session Speaker"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#each-config-inherits-the-cross-cutting-behavior-then-adds-entity-specifics","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Each config inherits the cross-cutting behavior, then adds entity specifics","x":"Every configuration's Configure method begins with base.Configure(builder) (for example SessionConfiguration.cs:18) and then adds its own mappings. That one base call is where…","i":"SessionQuestionAnswerConfiguration SpeakerQuestionAnswerConfiguration EventQuestionAnswerConfiguration SessionCategoryItemConfiguration SessionInvariants.TitleMaxLength SpeakerCategoryItemConfiguration ConferenceCategoryConfiguration SoftDeleteUniqueIndexConvention SponsorInvariants.NameMaxLength EventInvariants.NameMaxLength EventQuestionAnswer.EventId NullableEmailValueConverter"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#dbsets-the-context-shape-and-how-the-configurations-are-actually-found","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"DbSets, the context shape, and how the configurations are actually found","x":"ModuleApplicationDbContext (MMCA.ADC.Conference.Infrastructure/Persistence/DbContexts/ModuleApplicationDbContext.cs:19) is the Conference module's abstract DbContext. It does one…","i":"IEntityTypeConfigurationSQLServer MMCA.ADC.Conference.Service ModuleApplicationDbContext SessionQuestionAnswers SpeakerQuestionAnswer ApplicationDbContext EventQuestionAnswers SessionCategoryItems SpeakerCategoryItems dbo.OutboxMessages SQLServerDbContext SaveChangesAsync"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#seeding-two-real-events-always-sample-data-only-in-dev-and-ci","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Seeding: two real events always, sample data only in dev and CI","x":"ConferenceModuleDbSeeder (MMCA.ADC.Conference.Infrastructure/Persistence/DbContexts/Seeding/ConferenceModuleDbSeeder.cs:24) derives from the framework's DbSeeder and runs after…","i":"ConferenceModuleDbSeeder ConferenceModuleSeeder ManualIdRangeStart QuestionInvariants includeSampleData SessionInvariants ExistsAsync DbSeeder sf1nopko z1ecmzux Migrate"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#the-sessionize-adapter","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"The Sessionize adapter","x":"SessionizeService (MMCA.ADC.Conference.Infrastructure/Services/SessionizeService.cs:10) is a deliberately thin HTTP client: the whole class is one method. Given a Sessionize…","i":"EnsureSuccessStatusCode DependencyInjection SessionizeResponse SessionizeService HttpClient GetAsync code"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#the-anthropic-ai-scoring-adapter","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"The Anthropic AI scoring adapter","x":"AnthropicScoringService (MMCA.ADC.Conference.Infrastructure/Services/AnthropicScoringService.cs:16) is the richer of the two adapters: it scores one session proposal against a…","i":"CultureInfo.InvariantCulture OperationCanceledException AnthropicScoringService AnthropicContentBlock SessionScoringResult AnthropicResponse IAiScoringService AnthropicMessage AnthropicRequest JsonPropertyName AiScoreResponse LoggerMessage"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#scoring-runs-on-a-hosted-drain-guarded-across-replicas","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"Scoring runs on a hosted drain, guarded across replicas","x":"SessionScoringProcessor (MMCA.ADC.Conference.Infrastructure/Services/SessionScoringProcessor.cs:49) is the piece that makes a multi-minute paid AI pass safe to trigger from an…","i":"MMCA.ADC.Conference.Scoring scoring.run.failed.terminal ScoreEventSessionsCommand SessionScoringProcessor queue.MarkCompleted SessionScoringQueue BackgroundService CreateAsyncScope IDistributedLock TryAcquireAsync conferenceApp MarkCompleted"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#di-wiring-and-a-deliberate-resilience-override","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"DI wiring and a deliberate resilience override","x":"DependencyInjection (MMCA.ADC.Conference.Infrastructure/DependencyInjection.cs:11) is a single extension(IServiceCollection) block (the codebase's standard DI-registration idiom,…","i":"AddModuleConferenceInfrastructure RemoveAllResilienceHandlers StandardResilienceHandler DependencyInjection HttpClient.Timeout IServiceCollection extension"},{"u":"/docs/onboarding/group-19-conference-infrastructure.html#how-it-fits-together-at-runtime","d":"19. ADC Conference - Infrastructure & Persistence","k":"Onboarding Guide","t":"How it fits together at runtime","x":"Three flows tie the chapter together. Persistence flow: a Conference command handler mutates an aggregate and the unit of work saves; that resolves the concrete…","i":"Microsoft.EntityFrameworkCore.Metadata.Builders System.Text.Json.Serialization.JsonPropertyName CategoryInvariants.CategoryItemNameMaxLength MMCA.ADC.Conference.Infrastructure.Services Microsoft.Extensions.DependencyInjection MMCA.ADC.Migrations.SqlServer.Conference ApplyConfigurationsForEntitiesInContext SessionInvariants.AnswerValueMaxLength SpeakerInvariants.AnswerValueMaxLength QuestionInvariants.ManualIdRangeStart SpeakerQuestionAnswerConfiguration.cs EventInvariants.AnswerValueMaxLength"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","x":"This chapter is the edge of the Conference bounded context, the layer that turns the rich Conference domain (G17) and its CQRS slices (G18) into a running HTTP + gRPC surface,…","i":"MMCA.ADC.Conference.Contracts MMCA.ADC.Conference.Service MMCA.ADC.Conference.API ConferenceModuleSeeder ConferenceModule Microservices Readiness Contract Vertical IModule Design Rubric"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-controller-hierarchy-almost-everything-is-inherited","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The controller hierarchy, almost everything is inherited","x":"The Conference API exposes sixteen controllers, and the striking thing about them is how little code each carries. They split into three structural families, all built on the…","i":"sessionquestionanswerscontroller conferencecategoriescontroller ConferenceCategoriesController eventquestionanswerscontroller sessioncategoryitemscontroller speakercategoryitemscontroller SessionSelectionController sessionspeakerscontroller categoryitemscontroller eventspeakerscontroller PagedCollectionResult ServiceInfoController"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#authorization-at-the-edge-three-shapes-not-one","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Authorization at the edge, three shapes not one","x":"Authorization is capability-based by default but not uniform, and the differences are the interesting part. Most write-bearing controllers carry a class-level…","i":"AuthorizationPolicies.RequireAuthenticated ConferencePermissions.SpeakersManage SessionQuestionAnswersController EventQuestionAnswersController CurrentUserServiceExtensions IsPrivilegedConferenceReader AddModuleConferenceAPI ConferenceReadAudience HasPermissionAttribute SessionSelectionManage ConferencePermissions ICurrentUserService"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-request-records-the-inbound-write-shapes","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The request records, the inbound write shapes","x":"Several controllers declare small record class request types alongside themselves, co-located in the same file: AddRoomRequest/UpdateRoomRequest…","i":"updatesessionquestionanswerrequest updateeventquestionanswerrequest addsessionquestionanswerrequest addeventquestionanswerrequest addsessioncategoryitemrequest addspeakercategoryitemrequest updatecategoryitemrequest addsessionspeakerrequest addcategoryitemrequest addeventspeakerrequest SessionCreateRequest SessionsController"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#where-the-generic-shape-gives-way-filtering-warnings-and-calendars","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Where the generic shape gives way: filtering, warnings, and calendars","x":"SessionsController is the best illustration of how a controller earns its overrides. Every read action is [AllowAnonymous] and [OutputCache(PolicyName = \"SessionsCache\")]…","i":"BuildPublicSessionSpecificationAsync BuildPagedSessionSpecificationAsync GetSessionsBySpeakerFilterQuery GetPublicSessionFilterQuery GetPublicSponsorFilterQuery PublishedEventSpecification ExportSessionCalendarQuery EvictSessionsCacheAsync UpdateSponsorCommand HasDateRangeWarning IdempotentAttribute IOutputCacheFeature"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#two-more-deviations-versioning-and-decision-support","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Two more deviations, versioning and decision support","x":"ServiceInfoController exists to prove the API-versioning machinery works beyond a single version ([Rubric §9, API & Contract Design]). It is a one-member shell over Common's…","i":"ConferencePermissions.SessionSelectionManage SessionScoringEnqueueResult SessionSelectionController ServiceInfoControllerBase SessionScoringProcessor ServiceInfoController ISessionScoringQueue minimumSimilarity ConferenceCache AllowAnonymous AlreadyPending HandleFailure"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-module-entry-point-and-seeder-how-conference-plugs-in","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The module entry point and seeder, how Conference plugs in","x":"ConferenceModule is the Conference implementation of IModule. It is tiny by design: Register(...) calls the DependencyInjection extension's AddConferenceModule(...)…","i":"DisabledSessionBookmarkValidationService DisabledEventLiveValidationService ISessionBookmarkValidationService IEventLiveValidationService ConferenceErrorResources ConferenceModuleDbSeeder ConferenceModuleSeeder AuthorizationPolicies RegisterDisabledStubs RequireAuthenticated AddConferenceModule DependencyInjection"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-grpc-edge-conference-as-both-server-and-client","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The gRPC edge, Conference as both server and client","x":"When Conference is extracted into its own process, two of its in-process collaborations must cross a network boundary, and both are handled by the G13 transport boundary (Result…","i":"SessionBookmarkValidationServiceGrpcAdapter AddConferenceEventLiveValidationClient eventlivevalidationservicegrpcadapter AddConferenceSessionValidationClient ISessionBookmarkValidationService AddEngagementBookmarkCountClient ModuleLoader.DiscoverAndRegister eventlivevalidationgrpcservice GrpcResultExceptionInterceptor MMCA.ADC.Conference.Contracts MMCA.ADC.Conference.Service SessionBookmarksGrpcService"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-service-host-kestrel-first-and-why","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The service host: Kestrel first, and why","x":"The MMCA.ADC.Conference.Service Program.cs boots only the Conference module (Modules:Conference:Enabled=true). Kestrel is configured before anything else, and the whole of it is…","i":"builder.ConfigureEndpointsWithHealthProbe MMCA.ADC.Conference.Scoring MMCA.ADC.Conference.Service KestrelEndpointExtensions HttpProtocols.Http2 MapDefaultEndpoints HTTP_1_1_REQUIRED Http1AndHttp2 Program.cs UseSerilog httpGet GOAWAY"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#output-caching-and-warm-up-the-two-performance-extension-points","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"Output caching and warm-up, the two performance extension points","x":"Output caching is where this host carries the most bespoke configuration (Program.cs:191-255). The base policy is deny-by-default NoCache (Program.cs:198), so only explicitly…","i":"ConferenceReadAudience.PrivilegedRoles PublicEndpointOutputCachePolicy SelfHttpOutputCacheWarmupTask DbUpdateConcurrencyException SessionSelectionController ConferenceErrorResources AddPublicEndpointPolicy SelfHttpWarmupTaskBase ConferencePublicCache BookmarkCountsCache AddErrorResources Event.Name.Empty"},{"u":"/docs/onboarding/group-20-conference-api-grpc.html#the-runtime-picture-one-host-two-transports","d":"20. ADC Conference - API, gRPC Contracts & Service Host","k":"Onboarding Guide","t":"The runtime picture, one host, two transports","x":"After module discovery (Program.cs:313-319) the host wires the Engagement gRPC client (Program.cs:329), the broker (AddBrokerMessaging registering the UserRegistered…","i":"MMCA.Common.Application.Interfaces.Infrastructure currentUserService.IsPrivilegedConferenceReader ConferencePermissions.SessionSelectionManage AuthorizationPolicies.RequireAuthenticated ConferenceReadAudience.PrivilegedRoles.Any builder.ConfigureEndpointsWithHealthProbe DisabledSessionBookmarkValidationService MMCA.ADC.Conference.Shared.Authorization ConferencePermissions.ContentManagement GetPublicSessionCategoryItemFilterQuery GetPublicSpeakerCategoryItemFilterQuery AddConferenceEventLiveValidationClient"},{"u":"/docs/onboarding/group-21-conference-ui.html","d":"21. ADC Conference - UI","k":"Onboarding Guide","x":"What this chapter covers. This is the consumer half of the \"write-once UI, render everywhere\" story (primer §2): the Blazor pages and per-page HTTP services that turn the…","i":"MMCA.ADC.Conference.UI Architecture Responsive Component Design Rubric"},{"u":"/docs/onboarding/group-21-conference-ui.html#the-layering-inside-the-ui-a-page-never-touches-httpclient","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"The layering inside the UI: a page never touches HttpClient","x":"Each page is a .razor + .razor.cs code-behind pair that depends only on a UI service interface, never on HttpClient and never on the API's internals. The eight CRUD-shaped…","i":"IConferenceCategoryUIService RefreshFromSessionizeAsync ConferenceCategoryService EnsureSuccessStatusCode ICategoryItemUIService ServiceExceptionHelper PagedCollectionResult SponsorIdentifierType CategoryItemService IQuestionUIService EntityServiceBase ISessionUIService"},{"u":"/docs/onboarding/group-21-conference-ui.html#the-list-pages-derive-from-datagridlistpagebasetdto-get-everything-for-free","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"The list pages: derive from DataGridListPageBase, get everything for free","x":"Ten list screens, the organizer EventList, SessionList, SpeakerList, ConferenceCategoryList, QuestionList, RoomList, SponsorList, and the public PublicEventList,…","i":"MobileInfiniteScrollList ConferenceCategoryList DataGridListPageBase PublicSessionList PublicSpeakerList PublicSponsorList FetchMobilePage ListPageActions PublicEventList LoadServerData RestoreFilters GetPagedAsync"},{"u":"/docs/onboarding/group-21-conference-ui.html#container-and-presentational-split","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Container and presentational split","x":"The behaviour-heavy screens do not keep everything in one code-behind: the page stays the container (data fetching, filter and paging state, service calls) and hands rendering to…","i":"SessionSelectionSpeakerOverlap PublicSessionListFilterBar SpeakerCategoryItemsPanel SessionSelectionAiScores SessionSelectionDisplay PublicSessionListView PublicSessionList Architecture ReloadAsync Changed Testing Rubric"},{"u":"/docs/onboarding/group-21-conference-ui.html#child-and-join-entities-a-thin-postdelete-base","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Child-and-join entities: a thin POST/DELETE base","x":"Sessions, speakers, and events own join relationships (a speaker added to a session, a category item to a speaker) that the generic CRUD base cannot model, because the write…","i":"ISessionCategoryItemUIService ISpeakerCategoryItemUIService SessionCategoryItemService SpeakerCategoryItemService ISessionSpeakerUIService ChildEntityServiceBase IEventSpeakerUIService SessionSpeakerService EventSpeakerService MMCA.Common.UI DeleteAsync Validation"},{"u":"/docs/onboarding/group-21-conference-ui.html#display-enrichment-lookups-the-getall-vs-getbyid-populator-gap-worked-around-in-the-ui","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Display-enrichment lookups: the GetAll-vs-GetById populator gap, worked around in the UI","x":"Because the API's list endpoints do not always populate every cross-entity navigation, several pages need a cheap id-to-name map to render speaker names beside a session or an…","i":"ICategoryItemLookupService CategoryItemLookupService ISpeakerLookupService SpeakerLookupService SponsorshipPacketUrl IEventLookupService EventLookupService PublicSessionList CategoryItemInfo SessionSpeakers SpeakerInfo Dictionary"},{"u":"/docs/onboarding/group-21-conference-ui.html#three-feature-areas-that-go-beyond-crud","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Three feature areas that go beyond CRUD","x":"First, the speaker self-service dashboard: SpeakerDashboard is gated on the speakerid JWT claim (read from the cascaded authentication state and parsed as a Guid,…","i":"IOrganizerSessionFeedbackUIService IOrganizerEventFeedbackUIService OrganizerSessionFeedbackService OrganizerEventFeedbackService ISpeakerDashboardUIService AuthenticatedServiceBase OrganizerSessionFeedback SpeakerDashboardService OrganizerEventFeedback ServiceExceptionHelper IPublicLinkBuilder SpeakerDashboard"},{"u":"/docs/onboarding/group-21-conference-ui.html#session-selection-decision-support-the-asynchronous-edge","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Session-selection decision support, the asynchronous edge","x":"The most behaviour-rich page is the organizer-only SessionSelectionDashboard, which renders category distribution, speaker overlap, locality breakdown, and AI content-similarity…","i":"SessionSelectionFilterOptions ScoreEventSessionsResultDTO ISessionSelectionUIService ScoreEventSessionsCommand SessionSelectionDashboard SessionSelectionService CurrentEventSelector ScorePollTracker ScorePollSignal SessionsScored Resilience inherited"},{"u":"/docs/onboarding/group-21-conference-ui.html#public-versus-authenticated-rendering-and-the-device-capability-path","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Public versus authenticated rendering, and the device-capability path","x":"A recurring [Rubric §11, Security] pattern: the same conference entity is exposed through two page families. The public family (PublicEventList/PublicEventDetail,…","i":"IServiceProvider.GetService IConnectivityStatusService ISessionBookmarkUIService ConferenceReadAudience IHapticFeedbackService CurrentEventDefaults PublicSessionDetail PublicSpeakerDetail IScreenshotService CachedSessionPage PublicEventDetail PublicSessionList"},{"u":"/docs/onboarding/group-21-conference-ui.html#sponsors-a-feature-area-in-miniature","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Sponsors, a feature area in miniature","x":"The sponsor surface is worth reading as a compact tour of every pattern above, because it is the newest and touches all of them. Organizers manage the roster through SponsorList…","i":"ADCSponsorCollectionResult DataGridListPageBase EventLookupService PublicSponsorList ADCSponsorInfo Enum.GetValues SponsorCreate SponsorDetail SponsorList SponsorTier SponsorDTO ADCHome"},{"u":"/docs/onboarding/group-21-conference-ui.html#the-landing-page","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"The landing page","x":"ADCHome is the conference front door, shared by the web and MAUI heads; both serve the editorial images from their own site root today, so neither overrides the ImageBasePath…","i":"CurrentEventSelector ADCCollectionResult ConferenceTrackInfo KeynoteSpeakerInfo ImageBasePath ADCEventInfo Performance EventPhase Rendering ADCHome Rubric Timer"},{"u":"/docs/onboarding/group-21-conference-ui.html#routes-and-navigation","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"Routes and navigation","x":"All paths are centralized in ConferenceRoutePaths, a static catalogue of literal routes and id-parameterized builder methods (EventDetails(id), PublicSessionDetails(id),…","i":"ConferenceRoutePaths.EventDetails NavigationManager.NavigateTo NavigationPublicLinkBuilder EventFeedbackOrganizer ConferenceRoutePaths Internationalization PublicSessionDetails IPublicLinkBuilder IStringLocalizer SponsorVisitLink RoomCheckInLink SponsorDetails"},{"u":"/docs/onboarding/group-21-conference-ui.html#how-it-all-plugs-into-the-shell","d":"21. ADC Conference - UI","k":"Onboarding Guide","t":"How it all plugs into the shell","x":"Two registration types wire the area in. ConferenceUIModule implements Common's IUIModule (the front-end counterpart of the IModule back-end contract): it declares the module's…","i":"MMCA.ADC.Conference.UI.Pages.ConferenceCategory ConferenceRoutePaths.SessionSelectionDashboard MMCA.ADC.Conference.UI.Pages.SessionSelection ListPageActions.DeleteWithConfirmationAsync ArgumentException.ThrowIfNullOrWhiteSpace CurrentEventDefaults.SelectCurrentOrNext CurrentEventSelector.SelectCurrentOrNext DashboardService.GetSpeakerSessionsAsync ListPageActions.ReloadActiveLayoutAsync MMCA.ADC.Conference.UI.Pages.Feedback MMCA.ADC.Conference.UI.Pages.Question MMCA.ADC.Conference.UI.Pages.Session"},{"u":"/docs/onboarding/group-22-engagement-module.html","d":"22. ADC Engagement Module (Session Bookmarks)","k":"Onboarding Guide","x":"What this group covers. Engagement is the ADC bounded context that holds everything an attendee does at the conference beyond browsing the catalog. Four capability families live…","i":"MMCA.ADC.Engagement.Application.CheckIns.Services BookmarkCountService.BookmarkCountServiceClient MMCA.ADC.Engagement.Application.Points.Services MMCA.ADC.Engagement.Domain.UserSessionBookmarks MMCA.ADC.Engagement.Shared.UserSessionBookmarks MMCA.ADC.Engagement.Domain.Points.DomainEvents BookmarkCountService.BookmarkCountServiceBase MMCA.ADC.Engagement.Application.CheckIns.DTOs UserSessionBookmarkCacheEvictionHandlerTests assemblyProvider.GetConfigurationAssemblies AuthorizationPolicies.RequireAuthenticated CheckInsController.GetAttendanceStatsAsync"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","x":"What this chapter covers. This is the conference-day layer of the Engagement bounded context: the features that only matter while an event is actually happening in the room.…","i":"SessionQuestion PresenterView HappeningNow SessionLive LivePoll"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#the-two-aggregates-and-their-invariants","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"The two aggregates and their invariants","x":"Both aggregates are sealed AuditableAggregateRootEntity subclasses that follow the framework's factory-plus-Result discipline (primer §2). LivePoll…","i":"AuditableAggregateRootEntity SessionQuestion.Create SessionQuestionChanged SessionQuestionUpvote ToggleUpvoteHandler LivePollInvariants DomainEntityState LiveWindowEndUtc BaseDomainEvent CanAcceptUpvote CastVoteHandler LivePollChanged"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#the-write-path-and-where-the-realtime-broadcast-actually-happens","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"The write path, and where the realtime broadcast actually happens","x":"Each operation is a vertical slice under Application/{LivePollsSessionQuestions}/UseCases/{Op}/, and every command handler shares the first two beats: mutate the aggregate…","i":"SessionQuestionUpvoteChangedHandler ILiveChannelPublishQueue.Enqueue SessionQuestionUpvoteChanged LiveChannelPublishWorkItem LivePollVoteChangedHandler ILiveChannelPublishQueue ModerateQuestionHandler SessionQuestionChannel CreateLivePollHandler LivePollClosedPayload SubmitQuestionHandler CloseLivePollHandler"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#one-websocket-one-publisher-port-and-a-cross-service-ingress","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"One WebSocket, one publisher port, and a cross-service ingress","x":"The transport itself is framework-owned (ADR-039, Group 10). The single NotificationHub carries both durable notifications and channel events on one connection, and the…","i":"LiveChannelPublisherGrpcAdapter LiveChannelPublishProcessor SignalRLiveChannelPublisher RendererInfo.IsInteractive NullLiveChannelPublisher IPushNotificationSender LiveChannelGrpcService NotificationHubService ILiveChannelPublisher OnAfterRenderAsync OnInitializedAsync LeaveChannelAsync"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#the-read-path-and-how-the-ui-reacts","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"The read path and how the UI reacts","x":"Reads do not go through the generic entity-query machinery; the live views need shaped projections. LivePollResultsBuilder…","i":"LivePollNavigationPopulator SessionLiveModerationPanel SessionQuestionViewBuilder SessionLiveQuestionPanel LivePollResultsBuilder ISessionLiveUIService LivePollConfiguration CurrentEventSelector SessionLivePollPanel SessionLiveUIService GetOpenPollsHandler LivePollDTOMapper"},{"u":"/docs/onboarding/group-23-engagement-live-layer.html#authorization-feature-gating-and-the-cross-service-dependency-on-conference","d":"23. ADC Engagement Live Layer (Real-Time Polls & Session Q&A)","k":"Onboarding Guide","t":"Authorization, feature gating, and the cross-service dependency on Conference","x":"Both controllers, LivePollsController (MMCA.ADC.Engagement.API/Controllers/LivePollsController.cs:42) and SessionQuestionsController…","i":"MMCA.ADC.Engagement.Domain.LivePolls.DomainEvents MMCA.ADC.Engagement.Application.LivePolls.DTOs SessionQuestionChannel.QuestionUpvoteChanged MMCA.ADC.Engagement.Domain.SessionQuestions MMCA.ADC.Engagement.Shared.SessionQuestions AuthorizationPolicies.RequireAuthenticated LivePollInvariants.EnsureOptionTextIsValid PushNotificationSettings.ChannelKeyPattern MMCA.ADC.Engagement.UI.Pages.HappeningNow SessionQuestionPendingCountChangedPayload SessionQuestionUpvote.QuestionId.Required CurrentEventSelector.SelectCurrentOrNext"},{"u":"/docs/onboarding/group-24-identity-module.html","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","x":"What this chapter covers. This is the Identity bounded context of MMCA.ADC, the module that owns who a person is across every ADC surface: web, WebAssembly, and MAUI. It is a…","i":"GetUserPreferencesHandlerBase AuditableAggregateRootEntity AuthenticationServiceBase ChangePasswordHandlerBase ExportUserDataHandlerBase HasPermissionAttribute DeleteUserHandlerBase BaseIntegrationEvent SoftDeletedUserCache TIdentifierType IAnonymizable PiiAttribute"},{"u":"/docs/onboarding/group-24-identity-module.html#projects-one-bounded-context","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Projects, one bounded context","x":"The module is split along the standard Clean Architecture layering ([Rubric §3, Clean Architecture]), each project pinned by a trivial AssemblyReference / ClassReference anchor…","i":"MMCA.ADC.Identity.Infrastructure MMCA.ADC.Identity.Application ScanModuleApplicationServices MaxRegistrationsPerIpPerHour MMCA.ADC.Identity.Contracts ModuleApplicationDbContext MMCA.ADC.Identity.Service MMCA.ADC.Identity.Domain MMCA.ADC.Identity.Shared SoftDeletedUserValidator IdentityErrorResources IdentityModuleDbSeeder"},{"u":"/docs/onboarding/group-24-identity-module.html#the-user-aggregate-credentials-profile-and-cross-context-links-in-one-root","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"The User aggregate: credentials, profile, and cross-context links in one root","x":"User (MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Domain/Users/User.cs:33) is the only aggregate root in the module, and it carries more responsibility than most: it is…","i":"RegisterRequestValidator IPasswordChangeableUser DeviceFieldMaxLength UserPasswordChanged FirstNameMaxLength RefreshTokenExpiry RevokeRefreshToken UpdateRefreshToken LastNameMaxLength UpdatePreferences UserConfiguration CommonInvariants"},{"u":"/docs/onboarding/group-24-identity-module.html#authentication-a-thin-subclass-over-the-shared-engine","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Authentication: a thin subclass over the shared engine","x":"The login / registration / refresh / revocation workflow is not re-implemented here. It lives in AuthenticationServiceBase (G08), which owns the validate-first flow, the lockout…","i":"HttpContextExternalLoginEmailVerifier UnitOfWork.ExecuteInTransactionAsync CreateChangePreferencesCommand Auth.ExternalEmailNotVerified IdentityPermissions.UsersRead UserAccountAuthControllerBase CreateChangePasswordCommand IExternalLoginEmailVerifier AuthenticationServiceBase GetUserPreferencesHandler TChangePreferencesCommand ChangePreferencesCommand"},{"u":"/docs/onboarding/group-24-identity-module.html#the-privacy-pair-export-and-erasure","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"The privacy pair: export and erasure","x":"Two use cases make this module the codebase's clearest [Rubric §30, Compliance / Privacy / Data Governance] story, and both are now thin ADC specializations of a G14 base. The…","i":"UserDataExportEngagementSectionDTO NotificationUserDataExportSection EngagementUserDataExportSection IUserNotificationExportService IUserEngagementExportService BuildSubjectSnapshotAsync ExportUserDataHandlerBase UserDataExportSectionDTO UserDataExportSubjectDTO IUserDataExportSection OnAfterSoftDeleteAsync DeleteUserHandlerBase"},{"u":"/docs/onboarding/group-24-identity-module.html#avatars-the-third-mutating-slice","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Avatars: the third mutating slice","x":"The avatar trio is a small but complete example of a file-handling slice ([Rubric §11, Security] at the content boundary, ADR-045). UsersController caps the multipart upload at 2…","i":"RemoveUserAvatarHandler Avatar.InvalidUpload GetUserAvatarHandler SetUserAvatarHandler IFileStorageService ImageContentSniffer RequestSizeLimit IImageProcessor UsersController MaxAvatarBytes"},{"u":"/docs/onboarding/group-24-identity-module.html#persistence-seeding-and-the-disabled-stub","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Persistence, seeding, and the disabled stub","x":"ModuleApplicationDbContext (ModuleApplicationDbContext.cs:15) is the abstract, engine-agnostic context declaring the single Users set (:22); the concrete per-engine class…","i":"EntityTypeConfigurationSQLServer DisabledAttendeeQueryService IdentityModuleDbSeederBase ModuleApplicationDbContext IdentityModuleDbSeeder RegisterDisabledStubs ApplicationDbContext IdentityModuleSeeder EmailValueConverter dbo.OutboxMessages SQLServerDbContext UserConfiguration"},{"u":"/docs/onboarding/group-24-identity-module.html#crossing-the-service-boundary-grpc-and-integration-events","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"Crossing the service boundary: gRPC and integration events","x":"Identity talks to its peers two ways, and both live in Shared and Contracts so neither side reaches into the other's domain ([Rubric §7, Microservices Readiness]). Synchronously,…","i":"ConfigureEndpointsWithHealthProbe ModuleLoader.DiscoverAndRegister AttendeeQueryServiceGrpcAdapter SpeakerUnlinkedFromUserHandler SpeakerLinkedToUserHandler AddIdentityAttendeeClient KestrelEndpointExtensions RequireSuccessStatusCode SpeakerUnlinkedFromUser SelfHttpWarmupTaskBase AuthenticationService IAttendeeQueryService"},{"u":"/docs/onboarding/group-24-identity-module.html#the-ui-edge","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"The UI edge","x":"The Blazor surface is registered as an IdentityUIModule (MMCA.ADC.Identity.UI/IdentityUIModule.cs:13), an IUIModule descriptor that contributes two NavItems as resource keys, \"My…","i":"AuthenticatedServiceBase MobileInfiniteScrollList RetryPolicy.ExecuteAsync MMCA.Common.Testing.E2E DataGridListPageBase DependencyInjection IMediaPickerService IdentityRoutePaths IdentityUIModule ListPageActions IUserUIService UserListDTO"},{"u":"/docs/onboarding/group-24-identity-module.html#end-to-end-one-registration","d":"24. ADC Identity Module (Users, Profiles, GDPR Export/Erasure)","k":"Onboarding Guide","t":"End-to-end: one registration","x":"To see the chapter cooperate, follow a new attendee signing up. AuthController receives the register POST, captures the client IP for BR-213 rate limiting (AuthController.cs:57),…","i":"MMCA.ADC.Identity.Shared.Users.IntegrationEvents AttendeeQueryService.AttendeeQueryServiceClient System.Diagnostics.CodeAnalysis.SuppressMessage MMCA.ADC.Identity.Application.Users.Validation AttendeeQueryService.AttendeeQueryServiceBase AuthStateProvider.GetAuthenticationStateAsync LoginProtection__MaxRegistrationsPerIpPerHour ServiceCollectionDescriptorExtensions.Replace ListPageActions.DeleteWithConfirmationAsync MMCA.ADC.Identity.Domain.Users.DomainEvents ExternalAuthExtensions.ExternalLoginScheme System.Collections.Frozen.FrozenDictionary"},{"u":"/docs/onboarding/group-25-adc-host-composition.html","d":"25. ADC Application Host, UI Shell & Cross-Module Composition","k":"Onboarding Guide","x":"What this chapter covers. Every ADC module described so far, Conference, Engagement, Identity, Notification, is consumed somewhere. This chapter is that somewhere: the client…","i":"Microsoft.Extensions.Configuration.IConfiguration ArgumentException.ThrowIfNullOrWhiteSpace NowNextWidgetProvider.FetchSnapshotAsync MMCA.Common.UI.Components.Capabilities IPlatformApplication.Current.Services UIModuleConfiguration.IsModuleEnabled RemoteCertificateValidationCallback SessionCookieAuthenticationHandler EngagementRoutePaths.HappeningNow NowNextWidgetProvider.BuildViews System.Resources.ResourceManager WebAuthenticatorCallbackActivity"},{"u":"/docs/onboarding/group-26-device-capability-layer.html","d":"26. Device Capability Abstraction Layer (Native Contracts, MAUI, Browser & Fallback Adapters)","k":"Onboarding Guide","x":"What this chapter covers. One Blazor component library in MMCA.Common.UI renders on three very different heads: Blazor Server (server-side prerender plus interactive Server…","i":"MauiBackNavigationBridge.HandleBackPressedAsync MMCA.Common.UI.Services.Capabilities.Fallbacks MMCA.Common.UI.Services.Capabilities.Browser builder.Services.AddMauiDeviceCapabilities MauiLocalNotificationService.ScheduleAsync WebAuthenticator.Default.AuthenticateAsync ArgumentException.ThrowIfNullOrWhiteSpace Battery.Default.EnergySaverStatusChanged CommunityToolkit.Maui.Media.SpeechToText Connectivity.Current.ConnectivityChanged CultureInfo.DefaultThreadCurrentCulture ILocalNotificationService.ScheduleAsync"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","x":"What this group covers. Everything the codebase uses to prove itself: the four reusable test-support packages that ship out of MMCA.Common/Source/Hosting (MMCA.Common.Testing,…","i":"ServiceInfoVersioningContractTestsBase SqlServerIntegrationTestFixtureBase MMCA.Common.Testing.Architecture ProductionHostApplicationFactory AccessibilityViolationException DecoratorPipelineOrderTestsBase FeatureManagementTestExtensions ProblemDetailsContractTestsBase CapturingHttpMessageHandler CrossEntityNavigationFinder RouteAuthorizationTestsBase BunitInteractionExtensions"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#integration-tests-a-real-host-a-throwaway-database-a-per-test-reset","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Integration tests: a real host, a throwaway database, a per-test reset","x":"The integration tier boots the actual application, not a mock of it. The abstraction at its center is IIntegrationTestFixture (MMCA.Common.Testing/IIntegrationTestFixture.cs:8):…","i":"SqlServerIntegrationTestFixtureBase ProductionHostApplicationFactory FeatureManagementTestExtensions appsettings.Development.json SqlBaseEnvironmentVariable ConfigureTestFeatureFlags IEntityDataSourceRegistry CrossServiceFixtureBase IIntegrationTestFixture CrossServiceDataSource __EFMigrationsHistory WebApplicationFactory"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#architecture-fitness-functions-rules-that-gate-the-build","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Architecture fitness functions: rules that gate the build","x":"The layering and DDD conventions this codebase commits to are not left to code review, they are executed as tests. The reusable rule library lives in…","i":"CancellationTokenConventionTestsBase ConstructorDependencyCountTestsBase IntegrationEventContractTestsBase MMCA.Common.Testing.Architecture ObservabilityConventionTestsBase AggregateRootsHaveResultFactory MicroserviceExtractionTestsBase RawQueryableConventionTestsBase IdempotencyConventionTestsBase ArchitectureRules.Entities.cs AggregateConventionTestsBase CrossEntityNavigationFinder"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#component-tests-real-mudblazor-faked-edges","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Component tests: real MudBlazor, faked edges","x":"The bUnit tier renders a single Blazor component in-process with its real dependencies but stubbed network and auth. BunitComponentTestBase…","i":"IsAuthenticatedAuthorizationService AuthenticationStateProvider CapturingHttpMessageHandler BunitInteractionExtensions StubTokenStorageService BunitComponentTestBase FreshApiClientFactory MarkupSnapshotResult UiHttpServiceHarness AuthenticationState HttpMessageHandler IRenderedComponent"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#end-to-end-tests-a-real-browser-accessibility-and-performance-as-gates","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"End-to-end tests: a real browser, accessibility and performance as gates","x":"The E2E tier drives a real browser through Playwright. PlaywrightFixture (MMCA.Common.Testing.E2E/Infrastructure/PlaywrightFixture.cs:6) is an xUnit collection fixture (its…","i":"AssertNoAccessibilityViolationsAsync AccessibilityViolationException Wcag21AaExceptMudPagerCombobox ProfileManagementTestsBase GotoAndWaitForBlazorAsync UserRegistrationTestsBase UserPreferencesTestsBase ClickAndWaitForUrlAsync window.Blazor._internal AuthorizationTestsBase WaitForAuthResultAsync AuthenticatedUserPath"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#the-gallery-harness","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"The Gallery harness","x":"Component and E2E coverage of MMCA.Common's own UI needs a page to render, but the framework is not a runnable app. MMCA.Common.UI.Gallery is a deliberately backend-less Blazor…","i":"GalleryAuthenticationStateProvider GalleryFakeAuthenticationHandler StubNotificationInboxUIService StubPushNotificationUIService MMCA.Common.UI.E2E.Tests NullTokenStorageService MMCA.Common.UI.Gallery MapRazorComponents NullTokenRefresher NoOpAuthUIService MMCA.Common.slnx GalleryUIModule"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#contract-pipeline-and-benchmark-bases","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Contract, pipeline, and benchmark bases","x":"The last family pins guarantees that live in the composition of the stack rather than in any one type, and it is the subject of ADR-058: these suites ship in MMCA.Common.Testing…","i":"Application_ShouldNotDependOn_EntityFrameworkCore Controllers_ShouldNotDependOn_EntityFrameworkCore DataSubject_DeclaresPii_SoTheContractIsNotVacuous MMCA.Common.Architecture.Tests.CycleFixtures.Left Module_ShouldDeclare_ExpectedRequiresDependencies PiiRedactor_LeaksNoClearTextPii_ToLogsOrTelemetry CultureSwitch_ToSpanish_ShouldLocalizeAndPersist EveryRunbookAlertSection_MapsToAProvisionedAlert MobileViewport_CultureAndTheme_ShouldBeReachable ModuleShared_ShouldNotDependOn_OwnInternalLayers OpenApiDocument_DescribesEveryCorePublicResource Register_WithMismatchedPasswords_ShouldShowError"},{"u":"/docs/onboarding/group-27-testing-infrastructure.html#per-project-test-rollup","d":"27. Testing & Quality Infrastructure","k":"Onboarding Guide","t":"Per-project test rollup","x":"This guide treats tests as grouped, not sectioned per [Fact] (the logged exception in the charter): the reusable test bases, the shared architecture-fitness library and its…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests UserSessionBookmarkCacheEvictionHandlerTests PushNotificationProjectionTranslationTests SpecificationsDoNotNavigateToOtherEntities CachingDecoratorConstructorSelectionTests CurrentUserTargetingContextAccessorTests Microsoft.Extensions.DependencyInjection MMCA.ADC.Conference.Infrastructure.Tests MMCA.ADC.Engagement.Infrastructure.Tests MMCA.ADC.Notification.Application.Tests EntityServiceBaseIdempotencyRetryTests MMCA.ADC.CrossService.IntegrationTests"},{"u":"/docs/onboarding/devops-aspire.html","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","x":"This chapter teaches how the MMCA.ADC system goes from a single dotnet run on your workstation to a running stack of six .NET processes plus four containers: databases, a broker,…","i":"MMCA.Common.Aspire ServiceDefaults WithReference dotnet run"},{"u":"/docs/onboarding/devops-aspire.html#the-one-command-local-run","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"The one-command local run","x":"That command brings up everything the application needs locally: four SQL Server databases, Redis, RabbitMQ with management UI, a MailDev SMTP interceptor, four extracted…"},{"u":"/docs/onboarding/devops-aspire.html#mmcaadcapphost-the-orchestration-project","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"MMCA.ADC.AppHost, the orchestration project","x":"Source file: MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs Extension helpers: MMCA.Common.Aspire.Hosting/Extensions.cs (AddMessageBroker, WithBroker, WithJwksDiscovery,…","i":"identityService.WithE2eRegistrationThrottleLift LoginProtection__MaxRegistrationsPerIpPerHour ConnectionStrings__SQLServerConnectionString ConnectionStrings__CosmosConnectionString ConnectionStrings__SqliteConnectionString Authentication__JwtBearer__Authority identityService.WithEnvironment services__notification__grpc__0 WithE2eRegistrationThrottleLift E2E_LIFT_REGISTRATION_THROTTLE GrpcResultExceptionInterceptor JwtForwardingClientInterceptor"},{"u":"/docs/onboarding/devops-aspire.html#where-service-defaults-come-from-mmcacommonaspire-not-a-local-project","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Where service defaults come from, MMCA.Common.Aspire, not a local project","x":"There is no MMCA.ADC.ServiceDefaults project. The conventional Aspire \"ServiceDefaults\" shared project that scaffolding generates has been deleted; each service host (and the UI)…","i":"AddCommonKeyVaultConfiguration scoring.run.failed.terminal MMCA.ADC.ServiceDefaults AddCommonDataProtection DefaultAzureCredential builder.Configuration AuditTrailCleanupJob ConfigurationManager MapDefaultEndpoints AddServiceDefaults MMCA.Common.Aspire ScheduledJobRunner"},{"u":"/docs/onboarding/devops-aspire.html#mmcacommonaspire-the-framework-service-defaults-package","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"MMCA.Common.Aspire, the framework service-defaults package","x":"Source: MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs Telemetry: MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Telemetry/OutboxPollFilterProcessor.cs Security:…","i":"APPLICATIONINSIGHTS_CONNECTION_STRING OpenIdConnectMetadataWarmupTask EnableMultipleHttp2Connections AddInfrastructureHealthChecks Services.AddServiceDiscovery Telemetry__TracesSampleRatio ActivityTraceFlags.Recorded ConfigureHttpClientDefaults OTEL_EXPORTER_OTLP_ENDPOINT PooledConnectionIdleTimeout MMCA.Common.Infrastructure WarmupReadinessHealthCheck"},{"u":"/docs/onboarding/devops-aspire.html#mmcacommonaspirehosting-the-apphost-extensions-package","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"MMCA.Common.Aspire.Hosting, the AppHost extensions package","x":"Source: MMCA.Common/Source/Hosting/MMCA.Common.Aspire.Hosting/Extensions.cs This package lives in a separate assembly from MMCA.Common.Aspire so running services do not pull in…","i":"identityService.WithE2eRegistrationThrottleLift LoginProtection__MaxRegistrationsPerIpPerHour Authentication__JwtBearer__Authority WithE2eRegistrationThrottleLift E2E_LIFT_REGISTRATION_THROTTLE E2eRegistrationsPerIpPerHour builder.AddMessageBroker E2E_JWT_PRIVATE_KEY_PEM WithSQLServerDataSource E2E_JWT_PUBLIC_KEY_PEM Jwks__RsaPublicKeyPem Jwt__RsaPrivateKeyPem"},{"u":"/docs/onboarding/devops-aspire.html#the-six-dockerfiles","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"The six Dockerfiles","x":"All six Dockerfiles share the same multi-stage structure (base → build → publish → final) and the same base images. None build the AppHost, it is a local-only orchestration…","i":"MMCA.ADC.Notification.Service.dll MMCA.ADC.Conference.Service.dll MMCA.ADC.Engagement.Service.dll GlobalUsings.IdentifierType.cs MMCA.ADC.Identity.Service.dll MMCA.ADC.UI.Web.Client Directory.Build.props TreatWarningsAsErrors MMCA.ADC.Gateway.dll MMCA.ADC.UI.Web.dll MMCA.Common.Aspire MMCA.ADC.UI.Web"},{"u":"/docs/onboarding/devops-aspire.html#local-to-cloud-parity","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Local-to-cloud parity","x":"The AppHost topology maps directly to the Azure infrastructure provisioned by infra/main.bicep. The table below cross-references the local resource with its Azure equivalent: The…","i":"ConnectionStrings__SQLServerMigrationsAssembly APPLICATIONINSIGHTS_CONNECTION_STRING __SQLServerMigrationsAssembly OTEL_EXPORTER_OTLP_ENDPOINT ConnectionStrings__redis WithSQLServerDataSource Outbox__DatabaseName AddBrokerMessaging MessageBusProvider ADC_Notification AzureServiceBus ADC_Conference"},{"u":"/docs/onboarding/devops-aspire.html#the-yarp-gateways-role","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"The YARP Gateway's role","x":"The gateway (Source/Hosts/MMCA.ADC.Gateway) is a pure YARP reverse proxy. It has no DbContext, no ModuleLoader, no REST controllers, and no broker connection. Its Program.cs is…","i":"HttpResilienceDefaults.TotalRequestTimeout notificationRestConfig HttpVersion.Version20 RequestVersionOrLower RequestVersionExact restActivityTimeout ActivityTimeout Http1AndHttp2 VersionPolicy ForwardHttp2 MapForwarder ModuleLoader"},{"u":"/docs/onboarding/devops-aspire.html#startup-ordering-summary","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Startup ordering summary","x":"The health-based WaitFor chain imposes this ordering. Note that three of the four services wait on Identity without any explicit WaitFor in the AppHost: WithJwksDiscovery adds it…","i":"WithJwksDiscovery WithReference WaitFor"},{"u":"/docs/onboarding/devops-aspire.html#not-determinable-from-source","d":"Aspire Orchestration and Containers","k":"Onboarding Guide","t":"Not determinable from source","x":"- The specific integration events that flow over the broker (e.g., UserRegistered, SpeakerLinkedToUser) are cited from AppHost inline comments (Program.cs:46-51, 130-136), not…","i":"SpeakerLinkedToUser UserRegistered CLAUDE.md"},{"u":"/docs/onboarding/devops-cicd.html","d":"CI/CD and Operations","k":"Onboarding Guide","x":"This chapter walks the GitHub Actions workflows that govern MMCA, from the framework's continuous integration and lockstep NuGet release in MMCA.Common, through the ADC…","i":"MMCA.Common"},{"u":"/docs/onboarding/devops-cicd.html#mmcacommon-ciyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.Common, ci.yml","x":"File: MMCA.Common/.github/workflows/ci.yml The continuous-integration workflow for the MMCA.Common framework. Because the fifteen packages are consumed by every downstream…","i":"MMCA.Common.Infrastructure.Redis.Tests RestorePackagesWithLockFile Deque.AxeCore.Playwright Directory.Packages.props PLAYWRIGHT_BROWSERS_PATH DistributedCacheService MMCA.Common.Testing.E2E MMCA.Common.UI.Gallery Directory.Build.props TreatWarningsAsErrors Infrastructure.Tests MMCA.Common.UI.Tests"},{"u":"/docs/onboarding/devops-cicd.html#mmcacommon-releaseyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.Common, release.yml","x":"File: MMCA.Common/.github/workflows/release.yml The lockstep NuGet release workflow. When a maintainer pushes a vX.Y.Z git tag, this workflow deterministically derives the…","i":"Directory.Packages.props github.repository_owner DependencyVersionTests Testing.Architecture MMCA.Common.UI.Maui MMCA.Common.slnx GITHUB_REF_NAME Aspire.Hosting Infrastructure GITHUB_TOKEN Application release.yml"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-deployyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, deploy.yml","x":"File: MMCA.ADC/.github/workflows/deploy.yml The primary CI/CD pipeline for the Atlanta Developers Conference application. It runs on every push to main, on every pull request…","i":"needs.foundation.outputs.acrLoginServer coverage.integration.cobertura.xml MMCA.ADC.Integration.slnf Directory.Packages.props USE_MANAGED_IDENTITY_SQL JWT_RSA_PRIVATE_KEY_PEM MMCA.ADC.Services.Tests __EFMigrationsHistory Directory.Build.props SQL_LOCATION_OVERRIDE WebApplicationFactory skip_freshness_gates"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-e2eyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, e2e.yml","x":"File: MMCA.ADC/.github/workflows/e2e.yml The full-stack Playwright E2E test workflow. It brings up the complete Aspire stack (SQL Server + Redis + RabbitMQ + four services +…","i":"PLAYWRIGHT_BROWSERS_PATH MMCA.Common.Testing.E2E github.event.schedule WEB_VITALS_OUTPUT_DIR PlaywrightFixture workflow_dispatch matrix.browser WebVitalsTests workflow_call E2E_BASE_URL GITHUB_TOKEN E2E_BROWSER"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-cost-guardyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, cost-guard.yml","x":"File: MMCA.ADC/.github/workflows/cost-guard.yml A read-only FinOps check that confirms the production Azure footprint is at its cost baseline. It detects a specific operational…","i":"project_adc_2026_actual_load.md BASELINE_MAX_REPLICAS workflow_dispatch workflow_call deploy.yml production"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-load-testyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, load-test.yml","x":"File: MMCA.ADC/.github/workflows/load-test.yml A k6 load test targeting the output-cached Conference read endpoints through the production Gateway. It establishes a repeatable…","i":"project_adc_2026_actual_load.md workflow_dispatch inputs.peak_vus production base_url BASE_URL peak_vus PEAK_VUS"},{"u":"/docs/onboarding/devops-cicd.html#mmcaadc-cutover-per-service-dbsyml","d":"CI/CD and Operations","k":"Onboarding Guide","t":"MMCA.ADC, cutover-per-service-dbs.yml","x":"File: MMCA.ADC/.github/workflows/cutover-per-service-dbs.yml A one-time, manually-triggered workflow that migrated the four empty per-service databases (ADCIdentity,…","i":"inputs.freeze_traffic ADC_Notification OutboxProcessor ADC_Conference ADC_Engagement freeze_traffic OutboxMessages ADC_Identity containerapp GITHUB_TOKEN SqlBulkCopy deploy.yml"},{"u":"/docs/onboarding/devops-cicd.html#cross-workflow-summary","d":"CI/CD and Operations","k":"Onboarding Guide","t":"Cross-workflow summary","x":"(dr-drill.yml is the ADR-009 §29 restore drill: it PITR-restores a copy of a chosen database, times the restore for the RTO record, verifies it comes back Online, then deletes…","i":"workflow_call deploy.needs deploy.yml federated because e2e.yml subject deploy scoped false slnx the"},{"u":"/docs/onboarding/devops-cicd.html#rubric-category-index-for-this-chapter","d":"CI/CD and Operations","k":"Onboarding Guide","t":"Rubric category index for this chapter","i":"WebVitalsTests deploy.needs environment release.yml deploy.yml foundation production coverage cutover e2e.yml ci.yml deploy"},{"u":"/docs/onboarding/devops-iac.html","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","x":"This chapter teaches the Azure Infrastructure-as-Code layer for the MMCA.ADC application: what resources are provisioned, why they are shaped the way they are, how secrets reach…","i":"azure.yaml deploy.yml"},{"u":"/docs/onboarding/devops-iac.html#how-the-pieces-fit-together","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"How the pieces fit together","x":"Before diving into individual files, here is the end-to-end picture: Phases 1 and 2 are their own jobs (deploy.yml:747, deploy.yml:795) rather than steps inside deploy, so they…","i":"AZURE_RESOURCE_GROUP resourceGroup foundation main.bicep AtlDevCon deploy"},{"u":"/docs/onboarding/devops-iac.html#azureyaml-the-azd-project-definition","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"azure.yaml, the azd project definition","x":"File: MMCA.ADC/azure.yaml azure.yaml is the Azure Developer CLI (azd) manifest for the project. It declares six deployable services and points azd at the Bicep infrastructure…","i":"Directory.Packages.props foundation.bicep containerapp notification azure.yaml conference engagement main.bicep identity language provider context"},{"u":"/docs/onboarding/devops-iac.html#infrafoundationbicep-long-lived-shared-infrastructure","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"infra/foundation.bicep, long-lived shared infrastructure","x":"File: MMCA.ADC/infra/foundation.bicep Foundation is deployed first (CI/CD chapter: deploy.yml:773-779) on every run. It provisions three resources: the Azure Container Registry,…","i":"reference_log_analytics_sku_limits.md needs.foundation.outputs.acrName workspaceCapping.dailyQuotaGb appLogsConfiguration adminUserEnabled logAnalyticsName environmentName acrLoginServer resourceGroup resourceToken timerTriggers acrPurgeTask"},{"u":"/docs/onboarding/devops-iac.html#deployment-parameters-assembled-at-deploy-time-not-committed","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Deployment parameters, assembled at deploy time, not committed","x":"There is no infra/main.parameters.json file in the repository, the infra/ directory holds only foundation.bicep, main.bicep, DISASTER-RECOVERY.md, OPERATIONS.md,…","i":"USE_MANAGED_IDENTITY_SQL useManagedIdentitySql deploymentParameters SQL_ADMIN_PASSWORD alertEmailAddress foundation.bicep logAnalyticsName sqlAdminPassword environmentName Microsoft.Sql OPERATIONS.md hasAnthropic"},{"u":"/docs/onboarding/devops-iac.html#inframainbicep-the-full-application-infrastructure","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"infra/main.bicep, the full application infrastructure","x":"File: MMCA.ADC/infra/main.bicep main.bicep declares every application-layer Azure resource: Application Insights, the SLO scheduled query rules and their action group, two…","i":"ApplicationSettings__DatabaseInitStrategy Logging__OpenTelemetry__LogLevel__Default APPLICATIONINSIGHTS_CONNECTION_STRING AddHttpForwarderWithServiceDiscovery Authentication__JwtBearer__Authority project_outbox_cost_optimization.md Telemetry__DisableHttpClientMetrics project_adc_no_broker_in_azure.md Scheduler__PollingIntervalSeconds ObservabilityConventionTestsBase Telemetry__DisableRuntimeMetrics DataProtection__ApplicationName"},{"u":"/docs/onboarding/devops-iac.html#deployment-model-summary","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Deployment model summary","x":"The complete credential chain: No static credential exists at any link in this chain. The GitHub secrets AZURECLIENTID, AZURETENANTID, AZURESUBSCRIPTIONID are the OIDC…","i":"AZURE_SUBSCRIPTION_ID SQL_ADMIN_PASSWORD AZURE_CLIENT_ID AZURE_TENANT_ID secure"},{"u":"/docs/onboarding/devops-iac.html#rubric-category-cross-reference","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Rubric category cross-reference","x":"---","i":"useManagedIdentitySql OTEL_SERVICE_NAME adminUserEnabled KeyVault__Uri dailyQuotaGb minReplicas commonTags secrets secure false grpc"},{"u":"/docs/onboarding/devops-iac.html#not-determinable-from-source","d":"Infrastructure as Code, ADC Azure Deployment","k":"Onboarding Guide","t":"Not determinable from source","x":"- The exact AcrPull and Key Vault Secrets User role-assignment commands used in the out-of- band bootstrap are referenced in comments (main.bicep:915-919, main.bicep:933-936) but…","i":"USE_MANAGED_IDENTITY_SQL AZURE_RESOURCE_GROUP SQL_AAD_ADMIN_LOGIN AZURE_SQL_LOCATION SQL_AAD_ADMIN_OID deploymentMode deploy.yml main.bicep AcrPull Secrets westus2 false"},{"u":"/docs/onboarding/devops-runbooks.html","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","x":"This chapter covers every operational script and runbook in MMCA.ADC: the one-time Azure bootstrap, the database-per-service cutover story (how the legacy AtlDevCon monolith DB…","i":"MMCA.Store AtlDevCon MMCAStore MMCA.ADC ib_rg"},{"u":"/docs/onboarding/devops-runbooks.html#azure-setupsh-one-time-azure-bootstrap","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"azure-setup.sh, One-time Azure bootstrap","x":"File: MMCA.ADC/scripts/azure-setup.sh What it is. A bash script that creates every Azure identity and OIDC credential the GitHub Actions deploy pipeline needs. It is idempotent:…","i":"feedback_azure_cli_role_bug.md JWT_RSA_PRIVATE_KEY_PEM JWT_RSA_PUBLIC_KEY_PEM AZURE_SUBSCRIPTION_ID create_or_replace_fic AZURE_RESOURCE_GROUP MissingSubscription SQL_ADMIN_PASSWORD AZURE_CLIENT_ID AZURE_TENANT_ID Technologies assign_role"},{"u":"/docs/onboarding/devops-runbooks.html#the-database-per-service-cutover-story","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"The database-per-service cutover story","x":"Before the cutover scripts make sense, the story behind them does. Before ADR-006. All four modules (Identity, Conference, Engagement, Notification) pointed at a single shared…","i":"DataSources__Identity__SQLServerConnectionString CrossDataSourceDegradeConvention project_outbox_race_shared_db.md AtlDevCon.dbo.OutboxMessages inputs.freeze_traffic dbo.OutboxMessages workflow_dispatch ADC_Notification OutboxProcessor ADC_Conference ADC_Engagement freeze_traffic"},{"u":"/docs/onboarding/devops-runbooks.html#copy-atldevcon-to-per-service-dbsazureps1-azure-data-copy","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"copy-atldevcon-to-per-service-dbs.azure.ps1, Azure data copy","x":"File: MMCA.ADC/scripts/copy-atldevcon-to-per-service-dbs.azure.ps1 What it is. A PowerShell script that streams rows from AtlDevCon into the four per-service Azure SQL databases…","i":"Microsoft.Data.SqlClient AtlDevCon.schema.Table QUOTED_IDENTIFIER OutboxMessages KeepIdentity is_computed SqlBulkCopy sys.columns CHECKIDENT rowversion RowVersion AtlDevCon"},{"u":"/docs/onboarding/devops-runbooks.html#migrate-atldevcon-to-per-service-dbsps1-local-data-copy-wrapper","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"migrate-atldevcon-to-per-service-dbs.ps1, local data copy wrapper","x":"File: MMCA.ADC/scripts/migrate-atldevcon-to-per-service-dbs.ps1 What it is. A thin PowerShell wrapper that invokes the companion SQL script via sqlcmd against the local Aspire…","i":"QUOTED_IDENTIFIER AtlDevCon localhost sqlcmd error exit sql"},{"u":"/docs/onboarding/devops-runbooks.html#migrate-atldevcon-to-per-service-dbssql-local-sql-copy-script","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"migrate-atldevcon-to-per-service-dbs.sql, local SQL copy script","x":"File: MMCA.ADC/scripts/migrate-atldevcon-to-per-service-dbs.sql What it is. The T-SQL script that performs the actual per-row copy from AtlDevCon into the four per-service…","i":"AtlDevCon.sys.columns sys.identity_columns IDENTITY_INSERT OutboxMessages CHECKIDENT SchemaName XACT_ABORT AtlDevCon TableName timestamp TargetDb EXISTS"},{"u":"/docs/onboarding/devops-runbooks.html#infradisaster-recoverymd-dr-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/DISASTER-RECOVERY.md, DR runbook","x":"File: MMCA.ADC/infra/DISASTER-RECOVERY.md (175 lines; not the Store file of the same name) What it is. The authoritative disaster-recovery runbook for the ADC production…","i":"publicNetworkAccess scheduledQueryRules serviceDatabaseLtr workflow_dispatch ADC_Notification ADC_Conference ADC_Engagement resourceToken sloAlertSpecs ADC_Identity containerapp keyVaultUrl"},{"u":"/docs/onboarding/devops-runbooks.html#dr-drillyml-and-dr-restore-drillps1-the-adr-009-restore-drill","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"dr-drill.yml and dr-restore-drill.ps1, the ADR-009 restore drill","x":"Files: MMCA.ADC/.github/workflows/dr-drill.yml, MMCA.ADC/scripts/dr-restore-drill.ps1 What it is. The automation behind the drill requirement above: the workflow picks a target…","i":"workflow_dispatch SourceDatabase ADC_Identity deploy.needs deploy.yml AtlDevCon finally restore Online status exit show"},{"u":"/docs/onboarding/devops-runbooks.html#infraoperationsmd-day-2-alert-triage-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/OPERATIONS.md, day-2 alert triage runbook","x":"File: MMCA.ADC/infra/OPERATIONS.md What it is. The alert-to-action companion to the provisioned observability: what to do when each SLO alert fires, how to read the SLO workbook,…","i":"MMCA.Common.Testing.Architecture ObservabilityConventionTestsBase legacySloMetricAlertSpecs infra.OPERATIONS.md MinimumAlertSpecs infra.main.bicep OPERATIONS.md sloAlertSpecs ALERT_EMAIL AppTraces sloAlerts resource"},{"u":"/docs/onboarding/devops-runbooks.html#infrasql-managed-identitymd-staged-passwordless-sql-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/SQL-MANAGED-IDENTITY.md, staged passwordless-SQL runbook","x":"File: MMCA.ADC/infra/SQL-MANAGED-IDENTITY.md What it is. The runbook for moving the four service apps from SQL-login (password) auth to Entra managed-identity auth against their…","i":"vars.USE_MANAGED_IDENTITY_SQL USE_MANAGED_IDENTITY_SQL SQL_AAD_ADMIN_LOGIN SQL_AAD_ADMIN_OID Directory db_owner EXTERNAL Identity PROVIDER Managed Active CREATE"},{"u":"/docs/onboarding/devops-runbooks.html#infrapost-cutover-atldevcon-downgrademd-archive-downgrade-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"infra/POST-CUTOVER-atldevcon-downgrade.md, archive downgrade runbook","x":"File: MMCA.ADC/infra/POST-CUTOVER-atldevcon-downgrade.md What it is. A step-by-step runbook for the third and final commit of the database-per-service rollout: downgrading…","i":"maxSizeBytes ProcessedOn deploy.yml main.bicep AtlDevCon capacity against bacpac update query name NULL"},{"u":"/docs/onboarding/devops-runbooks.html#play-store-captureps1-android-screenshot-capture","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"play-store-capture.ps1, Android screenshot capture","x":"File: MMCA.ADC/scripts/play-store-capture.ps1 What it is. A PowerShell 7 script that captures a screenshot from an attached Android device or emulator via adb screencap and saves…","i":"screencap Files shell PATH slug adb png x86"},{"u":"/docs/onboarding/devops-runbooks.html#play-store-composeps1-play-store-screenshot-compositor","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"play-store-compose.ps1, Play Store screenshot compositor","x":"File: MMCA.ADC/scripts/play-store-compose.ps1 What it is. A PowerShell 7 script that reads raw captures from store-assets/play-store/raw/, wraps each into a 1080×1920 branded…","i":"System.Drawing.Common LinearGradientBrush brandTealDark brandCyan brandTeal imageMaxH imageMaxW slug png"},{"u":"/docs/onboarding/devops-runbooks.html#docsmobilereleaserunbookmd-store-submission-runbook","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"Docs/MobileReleaseRunbook.md, store-submission runbook","x":"File: MMCA.ADC/Docs/MobileReleaseRunbook.md What it is. The manual, credential-holding steps around a store submission that code and CI cannot perform, each tagged with when it…","i":"ADC_ANDROID_SIGNING_PASSWORD FileStorage.UploadFailed sha256_cert_fingerprints AndroidSigningStorePass com.ivanball.atldevcon grantAvatarStorageRole AndroidSigningKeyPass deployNotificationHub TargetPlatformVersion InternalServerError Entitlements.plist ivanball.AtlDevCon"},{"u":"/docs/onboarding/devops-runbooks.html#the-database-per-service-cutover-in-full-context","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"The database-per-service cutover in full context","x":"The five database-related artifacts above form a single coherent story, and the resilience artifacts extend it past the cutover: The AtlDevCon database is the thread that runs…","i":"CrossDataSourceDegradeConvention OPERATIONS.md deploy.yml main.bicep AtlDevCon delete NEVER sql"},{"u":"/docs/onboarding/devops-runbooks.html#rubric-tag-summary","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"Rubric tag summary","x":"---","i":"OPERATIONS.md"},{"u":"/docs/onboarding/devops-runbooks.html#not-determinable-from-source","d":"Operational Scripts & Runbooks","k":"Onboarding Guide","t":"Not determinable from source","x":"- ALERTEMAIL variable: DISASTER-RECOVERY.md:55-57 and OPERATIONS.md:8-11 both route alert notifications through the alertEmailAddress action-group receiver fed by the ALERTEMAIL…","i":"alertEmailAddress ALERT_EMAIL"},{"u":"/docs/onboarding/devops-testing.html","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","x":"Chapter scope note. The tier chapters (tier-00 through the sweep) document every type in the production codebase one by one. Test types are the logged exception: this chapter…","i":"Fact"},{"u":"/docs/onboarding/devops-testing.html#1-solution-composition-and-the-test-runner","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"1. Solution composition and the test runner","x":"The two deployed apps use the same two-file pattern; MMCA.Common and MMCA.Helpdesk ship a .slnx only, because their solutions are already fast enough not to need a CI subset:…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests MMCA.ADC.CrossService.IntegrationTests MMCA.ADC.Architecture.Tests MMCA.Store.Integration.slnf MMCA.ADC.Integration.slnf MMCA.ADC.IntegrationTests DistributedCacheService MMCA.ADC.Services.Tests MMCA.ADC.Gateway.Tests MMCA.ADC.WebAPI.Tests MMCA.Common.API.Tests WebApplicationFactory"},{"u":"/docs/onboarding/devops-testing.html#2-test-project-layout","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"2. Test project layout","x":"The inventory below is drawn from 00-inventory.md:23-117 (test-assembly counts) and the solution files above. Counts are distinct types per project as reported by the Roslyn…","i":"MMCA.ADC.ServiceBusEmulator.IntegrationTests CurrentEventNotificationScopeProviderTests MMCA.ADC.Conference.Infrastructure.Tests MMCA.ADC.Engagement.Infrastructure.Tests MMCA.ADC.Notification.Application.Tests MMCA.ADC.CrossService.IntegrationTests MMCA.ADC.Identity.Infrastructure.Tests MMCA.ADC.Notification.IntegrationTests MMCA.Common.Infrastructure.Redis.Tests NotificationUserDataExportSectionTests MMCA.ADC.Conference.Application.Tests MMCA.ADC.Engagement.Application.Tests"},{"u":"/docs/onboarding/devops-testing.html#3-shipped-testing-infrastructure-packages","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"3. Shipped testing-infrastructure packages","x":"MMCA.Common ships four of its fifteen packages as testing infrastructure that downstream apps consume as NuGet references rather than writing their own harness…","i":"AxeOptions.Wcag21AaExceptMudPagerCombobox WebApplicationFactory.ConfigureServices ServiceInfoVersioningContractTestsBase AssertNoAccessibilityViolationsAsync IsAuthenticatedAuthorizationService SqlServerIntegrationTestFixtureBase MutableAuthenticationStateProvider PageExtensions.FillAndVerifyAsync MMCA.Common.Testing.Architecture ProductionHostApplicationFactory AccessibilityViolationException DecoratorPipelineOrderTestsBase"},{"u":"/docs/onboarding/devops-testing.html#4-architecture-fitness-tests-executable-governance","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"4. Architecture fitness tests, executable governance","x":"[Rubric §34, Architecture Governance & Documentation]: §34 assesses whether architectural decisions are documented, enforced, and kept honest over time; fitness functions are the…","i":"AggregateRoots_ShouldHave_NoPublicConstructors SpecificationsDoNotNavigateToOtherEntities ArchitectureRules.PinnedPackageMajorBelow LayerMap_ModulesDeclareEveryExpectedLayer MassTransit_MustNotExceed_MajorVersion8 CoreLayers_ShouldNotDependOn_Transport ImageSharp_MustNotExceed_MajorVersion3 ObservabilityConventionTestsBaseTests Infrastructure_ShouldNotDependOn_Api ConstructorDependencyCountTestsBase DomainFactories_ShouldReturn_Result FakeDependentModuleConformanceTests"},{"u":"/docs/onboarding/devops-testing.html#5-integration-and-e2e-strategy","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"5. Integration and E2E strategy","x":"The four integration test projects (Identity, Conference, Engagement, Notification) each boot their service in-process with WebApplicationFactory . The lifecycle is not written…","i":"MMCA.Store.ServiceBusEmulator.IntegrationTests MMCA.ADC.ServiceBusEmulator.IntegrationTests MMCA.Store.CrossService.IntegrationTests MMCA.ADC.CrossService.IntegrationTests MMCA.Common.Infrastructure.Redis.Tests AssertNoAccessibilityViolationsAsync IntegrationTestBase.InitializeAsync SqlServerIntegrationTestFixtureBase MMCA.Common.Infrastructure.Tests IdentityIntegrationTestFixture appsettings.Development.json DatabaseInitStrategy.Migrate"},{"u":"/docs/onboarding/devops-testing.html#6-worked-examples","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"6. Worked examples","x":"Three examples tie the infrastructure above to real test code. The per-repo class is a bare subclass; the facts, the package lists and the parsing live once in the shared base:…","i":"IdentityIntegrationTestFixture.DisposeAsync ImageSharp_MustNotExceed_MajorVersion3 IntegrationTestBase.InitializeAsync MutableAuthenticationStateProvider IntegrationTestBase.DisposeAsync IdentityIntegrationTestFixture AuthenticationStateProvider GetAuthenticationStateAsync IdentityIntegrationTestBase Fixture.ResetDatabaseAsync Directory.Packages.props AuthenticateAsAttendee"},{"u":"/docs/onboarding/devops-testing.html#7-the-tiers-and-the-gates-that-run-them","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"7. The tiers and the gates that run them","x":"A test tier only means something once you know what it blocks. This is the map. MMCA.Common's ui-e2e job (MMCA.Common/.github/workflows/ci.yml:228) builds the out-of-slnx gallery…","i":"Integration.slnf MemoryDiagnoser E2E_BROWSER browsers chromium coverage CI.slnf e2e.yml firefox skipped success deploy"},{"u":"/docs/onboarding/devops-testing.html#quick-reference-rubric-categories-touched-in-this-chapter","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"Quick reference: rubric categories touched in this chapter","x":"---"},{"u":"/docs/onboarding/devops-testing.html#cross-links","d":"Testing Architecture & Solution Composition","k":"Onboarding Guide","t":"Cross-links","x":"- Primer: 00-primer.md5-the-solution--test-layout , solution files, MTP runner, slnx-excluded UI projects - Primer:…","i":"MMCA.ADC.Integration.slnf IIntegrationTestFixture"},{"u":"/docs/onboarding/99-coverage-audit.html","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","x":"This audit reconciles the written guide against the mechanically-extracted inventory, logs every deliberate exception, verifies the grouping/ordering rules, proves all 34 rubric…","i":"classify.ps1 verify.ps1 plan.ps1"},{"u":"/docs/onboarding/99-coverage-audit.html#1-coverage-reconciliation","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"1. Coverage reconciliation","x":"Cross-check result: verify.ps1 confirms 0 of the 1,890 individually-sectioned types are missing from their group chapter, every one appears as a heading or in a sibling-family…","i":"SpecificationsDoNotNavigateToOtherEntities UserSessionBookmarkCacheEvictionHandler MMCA.Common.Infrastructure.Redis.Tests DatabaseInitializationExtensionsTests HttpContextExternalLoginEmailVerifier MMCA.ADC.Conference.Application.Tests MMCA.ADC.Engagement.Application.Tests ObservabilityConventionTestsBaseTests OwnSessionQuestionAnswerSpecification AnonymousAuthenticationStateProvider AzureNotificationHubNativePushSender CancellationTokenConventionTestsBase"},{"u":"/docs/onboarding/99-coverage-audit.html#2-exceptions-log-every-deliberate-omission-with-reason","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"2. Exceptions log (every deliberate omission, with reason)","x":"EF Core migrations (/Migrations/, .Migrations.SqlServer), ModelSnapshot, .Designer.cs, .g.cs, GlobalUsings.g.cs, and AssemblyInfo.cs are excluded by rule (Tools/invtool…","i":"ObservabilityConventionTestsBase ProductionHostApplicationFactory RouteAuthorizationTestsBase ModuleConformanceTestsBase DependencyInjectionAssert GracefulShutdownTestsBase MMCA.Common.Benchmarks Migrations.SqlServer Testing.Architecture MMCA.Common.Testing GlobalUsings.g.cs AssemblyInfo.cs"},{"u":"/docs/onboarding/99-coverage-audit.html#3-grouping--ordering-verification","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"3. Grouping & ordering verification","x":"- Every type in exactly one group. classify.ps1 assigns all 3,465 nodes via name-level overrides (for the grab-bag MMCA.Common.Interfaces/Services namespaces) + ordered…","i":"MidSaveContextCreatingDbContext OutboxRoutingTestDbContext ReentrantSaveInterceptor FailingSaveInterceptor INavigationPopulator ResultGrpcExtensions EntityQueryService SelfHttpWarmupTask ApiControllerBase DeferredDispatch ErrorHttpMapping _typemap.tsv"},{"u":"/docs/onboarding/99-coverage-audit.html#4-rubric-coverage-matrix","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"4. Rubric coverage matrix","x":"Every one of the 34 categories is explained at least once against real code. \"First explained in\" is the earliest group chapter (by order) that tags it; many recur and several…","i":"ThemeService verify.ps1 token"},{"u":"/docs/onboarding/99-coverage-audit.html#5-open-questions--not-determinable-from-source","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"5. Open questions / not determinable from source","x":"1. IDbSeeder host invocation (group-07). The seeding contract and implementations are in MMCA.Common, but the IHostedService/startup invoker that actually runs seeding at boot…","i":"MMCA.ADC.Identity.Contracts.DependencyInjection ModuleApplicationDbContext CrossSourceSpecification ReadRepositoryExtensions EntityTypeConfiguration DependencyInjection DbContexts.Factory ChangePassword ExportUserData IHostedService EnsureCreated IUnitOfWork"},{"u":"/docs/onboarding/99-coverage-audit.html#6-how-to-regenerate-this-audit","d":"Phase 4, Coverage Audit","k":"Onboarding Guide","t":"6. How to regenerate this audit","x":"Then copy the refreshed out/00-inventory.md and out/00-dependency-manifest.md into Docs/Onboarding/ (the 00-group-taxonomy.md is written there directly by classify.ps1).","i":"classify.ps1"},{"u":"/docs/onboarding/CONCEPT-MAPS.html","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","x":"Mermaid diagrams distilled from the Onboarding guide (primer, group taxonomy, dependency manifest, and the 27 group chapters). Each diagram captures a relationship between the…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#1-system-context-two-codebases--the-15-packages","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"1. System context, two codebases + the 15 packages","x":"MMCA.Common is a framework published as fifteen NuGet packages in lockstep, to nuget.org and GitHub Packages from one tag (ADR-053); MMCA.ADC and MMCA.Store consume them. The…","i":"MMCA.Common.slnx MMCA.Common MMCA.Store MMCA.ADC UI.Maui"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#2-clean-architecture-the-layered-dependency-rule","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"2. Clean Architecture, the layered dependency rule","x":"Source dependencies point inward toward the Domain; each layer references only layers below it. Deliberate exceptions: UI and Grpc depend on Shared only (UI for Blazor WASM…","i":"ProjectReference UI.Maui Aspire Blazor bridge depend Shared above host only sits and"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#3-the-27-functional-groups-dependency--build-order","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"3. The 27 functional groups, dependency / build order","x":"The primary axis of the guide: every type lives in exactly one of 27 chapter groups, ordered roughly topologically. Foundational, widely-depended-on concerns first (Result →…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#4-core-framework-patterns-how-the-building-blocks-compose","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"4. Core framework patterns, how the building blocks compose","x":"The pattern-level view of the same backbone: the ideas the primer commits to and how they feed each other. Result is the pervasive currency; DDD blocks produce domain events;…","i":"Result"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#5-request-lifecycle-the-cqrs-decorator-pipeline-adr-014","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"5. Request lifecycle, the CQRS decorator pipeline (ADR-014)","x":"Handlers are thin (one method); every cross-cutting concern is a decorator wrapping the next. Scrutor TryDecorate composes them in reverse registration order (last registered =…","i":"AddApplicationDecorators TryDecorate"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#6-event-driven-integration-outbox-dual-dispatch-adr-003--010--021","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"6. Event-driven integration, outbox dual-dispatch (ADR-003 / 010 / 021)","x":"Domain events are captured into an OutboxMessage row in the same transaction as the data (no dual-write bug). The two event kinds then part ways: local domain events are…","i":"IIntegrationEventPublisher IEventBus.PublishAsync OutboxProcessor OutboxMessage SchemaVersion IMessageBus MessageId"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#7-modular-monolith--extractable-services-adr-006--007--008--012","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"7. Modular monolith → extractable services (ADR-006 / 007 / 008 / 012)","x":"Modules implement IModule and are discovered + Kahn-ordered by ModuleLoader (ADR-059). The same module code runs as a single monolith host or as N service processes behind a YARP…","i":"MMCA.ADC.WebAPI ModuleLoader IMessageBus IModule"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#8-persistence-database-per-service--polyglot-engines-adr-006--018--030","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"8. Persistence, database-per-service + polyglot engines (ADR-006 / 018 / 030)","x":"One concrete SQLServerDbContext over the abstract ApplicationDbContext, one instance per database. Each entity is engine-agnostic; a single [UseDataSource(engine)] attribute on…","i":"ApplicationDbContext SQLServerDbContext UseDataSource engine"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#9-authentication--authorization-stack","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"9. Authentication & Authorization stack","x":"The auth concern (G08) spans token validation, session cookies, federated sign-in, password hashing, brute-force protection, refresh-token rotation and revocation, and a layered…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#10-notifications-three-channels-behind-one-send-pipeline-adr-024--044","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"10. Notifications, three channels behind one send pipeline (ADR-024 / 044)","x":"One use case (SendPushNotificationHandler) writes a durable per-user inbox, fires a transient SignalR push, and then an OS-level native push that reaches a backgrounded or killed…","i":"SendPushNotificationHandler MMCA.ADC.Notification SmtpEmailSender IEmailSender"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#11-ui-write-once-render-everywhere--i18n--theming","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"11. UI, write-once render everywhere + i18n + theming","x":"A page is authored once as a Razor component in a per-module UI library; both the Blazor web host (Server + WASM) and the .NET MAUI host reference the same libraries, so it…","i":"IStringLocalizer InteractiveAuto MMCA.Common.UI ThemeService rendermode"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#12-adc-business-modules-bounded-contexts-end-to-end","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"12. ADC business modules, bounded contexts end-to-end","x":"Each ADC module is a vertical slice through all layers. Conference is large enough to split across five chapters (G17-G21); Engagement takes two (G22 session bookmarks, G23 the…"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#13-the-adrs-grouped-by-theme","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"13. The ADRs, grouped by theme","x":"Every accepted ADR in Website/docs-src/adr/, clustered by the concern it governs. That directory's README.md is the canonical index and owns the count and range; this map only…","i":"README.md"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#14-the-34-category-evaluation-rubric","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"14. The 34-category evaluation rubric","x":"The lens the guide tags code against ([Rubric §N]). Scored on two axes: Maturity (0-4, process) and Implementation (0-10, substance). Three parts. ---","i":"Rubric"},{"u":"/docs/onboarding/CONCEPT-MAPS.html#15-how-the-axes-fit-together-reading-map","d":"MMCA Concept & Pattern Maps","k":"Onboarding Guide","t":"15. How the axes fit together (reading map)","x":"The guide is organized on two axes at once. This ties the diagrams above back to the guide's navigation. --- - Group-to-group arrows in §3 show the dominant \"builds on\" direction…","i":"ApplicationDbContext"},{"u":"/docs/governance/index.html","d":"Architecture Governance","k":"Architecture Governance","x":"The governance artifacts behind the MMCA platform: the shared 34-category evaluation rubric, and each repo's evidence-based scorecard plus its remediation backlog. Every score…"},{"u":"/docs/governance/index.html#the-rubric","d":"Architecture Governance","k":"Architecture Governance","t":"The rubric","x":"- Architecture Evaluation Criteria: the 34-category rubric (Maturity 0-4 and Implementation 0-10 per category) that all three application repos are scored against."},{"u":"/docs/governance/index.html#how-these-are-maintained","d":"Architecture Governance","k":"Architecture Governance","t":"How these are maintained","x":"Scores are re-verified from source on a cadence: each category is scored by reading the current code, config, and CI (never rolled forward), and any change lands with the…"},{"u":"/docs/governance/adc-ArchitectureScorecard.html","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","x":"Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.ADC's architecture scores (replaces the former single-axis snapshot; see git history).…","i":"dotnet_analyzer_diagnostic.severity SessionSelectionDashboard.razor.cs ArchitectureEvaluationCriteria.md MMCA.ADC.Notification.Application MMCA.Common.Testing.Architecture UIArchitectureConventionTests.cs StateManagementConventionTests SQLitePCLRaw.bundle_e_sqlite3 UIArchitectureConventionTests ObservabilityConventionTests PseudoLocalizationTests RemediationBacklog.md"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#executive-summary","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Executive summary","x":"MMCA.ADC is a mature, evidence-driven .NET 10.0 (LangVersion preview) DDD/Clean Architecture system for the Atlanta Developers Conference, deliberately extracted from a modular…","i":"MMCA.ADC.CrossService.IntegrationTests MMCA.ADC.Notification.IntegrationTests SessionIncludeChildrenRegressionTests UIArchitectureConventionTestsBase FrameworkVersionConsistencyTests LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture ErrorMessages.ValidationError LocalizedTextConventionTests ObservabilityConventionTests SpecificationConventionTests BlazorCspPolicyProvider.cs"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#scorecard","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Scorecard","x":"Weighted column = Maturity·weight / Implementation·weight per row. Axis-gap findings: the former §21 gap (excellent-but-not-enforced, impl 7 over maturity 2) closed on 2026-07-02…","i":"MmcaCultureBootstrap.SetBrowserCultureAsync ResilienceCircuitBreakerFaultInjectionTests MMCA.ADC.CrossService.IntegrationTests dotnet_analyzer_diagnostic.severity FrameworkVersionConsistencyTests.cs StateManagementConventionTestsBase MMCA.ADC.Notification.Application UIArchitectureConventionTestsBase MMCA.Common.Testing.Architecture ConstructorDependencyCountTests LocalizedTextConventionTests.cs ObservabilityConventionTests.cs"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#indices","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Indices","x":"- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 311 ÷ 320 = 97.2% (down from 97.8%, 313/320: the twenty-second-cycle full re-score (2026-07-21, pin v1.121.0) corrected §22…"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#top-5-strengths","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 strengths","x":"1. Architecture intent is executable, not prose: 29 architecture-test classes / 91 methods gate CI (§34 Governance, mat 4 / impl 9, and §3 Clean Architecture, mat 4 / impl 9):…","i":"SessionIncludeChildrenRegressionTests MMCA.Common.Testing.Architecture SpecificationConventionTests.cs AddSessionCookieAuthentication StateManagementConventionTests MicroserviceExtractionTests AddCommonSecurityHeaders ArchitecturalAnalysis.md LayerDependencyTests AddCommonBlazorCsp DataResidencyTests DomainPurityTests"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#top-5-risks","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 risks","x":"Expected-delta note (updated 2026-08-01): several entries below record an expected lift of \"impl 9→10\". Under the 2026-08-01 recalibration those are attainable, not aspirational:…","i":"publicNetworkAccess packages.lock.json MMCA.ADC.CI.slnf deploy.needs maxReplicas MMCA.ADC.UI CI.slnf"},{"u":"/docs/governance/adc-ArchitectureScorecard.html#cross-repo-comparison","d":"MMCA.ADC: Architecture Scorecard","k":"Architecture Governance","t":"Cross-repo comparison","x":"How this repo's quality relates to the other consumers (where the framework's quality propagates, where a repo diverges or under-uses it, and where a consumer does better than…"},{"u":"/docs/governance/adc-RemediationBacklog.html","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","x":"Derived from ArchitectureScorecard.md (single-axis 0-4, baseline 75%, 241/320, dated 2026-06-08). Current authoritative two-axis scores (twenty-sixth-cycle full re-score,…","i":"MMCA.ADC.CrossService.IntegrationTests SqlServerIntegrationTestFixtureBase SessionSelectionDashboard.razor.cs MMCA.ADC.Notification.Application FrameworkVersionConsistencyTests StateManagementConventionTests UIArchitectureConventionTests IntegrationTestReworkPlan.md LocalizedTextConventionTests ObservabilityConventionTests TranslationCompletenessTests MMCA.ADC.Integration.slnf"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-6-highest-leverage","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔴 Priority 6: highest leverage","x":"Token handling uses two rubric-named anti-patterns, with no CSP defense-in-depth. Status (2026-06-27): cookie-only refresh + in-memory access (auth-path BFF), OAuth…","i":"ResilienceCircuitBreakerFaultInjectionTests DisconnectedCircuitRetentionPeriod ManagementRouteAuthorizationTests GatewaySecurityHeadersMiddleware E2E_LIFT_REGISTRATION_THROTTLE OAuthController.CompleteAsync OAuthController.ExchangeAsync SameOriginProxyTokenRefresher MMCA.ADC.Conference.UI.Tests AuthenticationStateProvider EventDetailPage.StatusChip InvalidOperationException"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-4","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Priority 4","x":"The (4−score)×weight formula puts this at 4, but the High flag is a contractual/regulatory exposure that contradicts a shipped, publicly-served policy: treat it as do-soon. -…","i":"user_notification_export.proto LocalizedTextConventionTests TranslationCompletenessTests user_engagement_export.proto ExportUserDataHandlerTests ErasureAndPiiLoggingTests DeleteUserHandlerTests ErrorMessages.Success SessionQuestionAnswer User.PreferredCulture UserRegisteredHandler EventQuestionAnswer"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-3-score-3-weight-3-one-rung-from-a-4","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟡 Priority 3: score 3, weight 3 (one rung from a 4)","x":"- ~~(High) The 258-test Testcontainers integration tier references the deleted MMCA.ADC.WebAPI host, won't build, and is excluded.~~ RESOLVED: reworked as per-service…","i":"Update_WithStaleRowVersion_ReturnsConflict MMCA.ADC.CrossService.IntegrationTests SessionSelectionDashboard.razor.cs StateManagementConventionTestsBase ManagementRouteAuthorizationTests UIArchitectureConventionTestsBase InProcessEventBus.PublishAsync SessionSelectionSpeakerOverlap StateManagementConventionTests UIArchitectureConventionTests DbUpdateConcurrencyException PublicSessionList.razor.cs"},{"u":"/docs/governance/adc-RemediationBacklog.html#priority-2-score-3-weight-2-polish--hardening","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Priority 2: score 3, weight 2 (polish / hardening)","x":"- ~~(Medium) No OpenAPI served by any running service, yet CLAUDE.md still advertises 4 doc UIs (/swagger, /nswag-swagger, /api-docs, /scalar/v1).~~ - [x] Serve OpenAPI per…","i":"Microsoft.AspNetCore.Authorization.Authorize AuthorizationPolicies.RequireOrganizer MMCA.ADC.Conference.IntegrationTests ManagementRouteAuthorizationTests FrameworkVersionConsistencyTests IdentityRouteAuthorizationTests IntegrationEventContractTests MMCA.ADC.Migrations.SqlServer Microsoft.AspNetCore.OpenApi ObservabilityConventionTests MicroserviceExtractionTests Validation.CorrectFollowing"},{"u":"/docs/governance/adc-RemediationBacklog.html#implementation-band-implementation--8-ranked-by-implpriority","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔵 Implementation band (implementation <= 8, ranked by implPriority)","x":"Added 2026-07-28 when the ledger gained its second ranked axis. Until then implementation gaps appeared only as unranked sub-bullets inside maturity items, so they were never…","i":"SessionSelectionDashboard.razor.cs MMCA.ADC.Notification.Application AdcArchitectureMap.DefineLayers ConferenceCategoryDetail.razor HappeningNow.razor.cs TreatWarningsAsErrors DeviceSettings.razor SponsorCreate.razor SponsorDetail.razor System.Private.Uri workflow_dispatch UI.Web.Client"},{"u":"/docs/governance/adc-RemediationBacklog.html#resolved-2026-07-25-performance-program-2","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved 2026-07-25 (performance program 2)","x":"Second evidence-led performance pass over Common/ADC/Store. ADC's share shipped as three PRs plus the v1.127.0 framework sweep. - [x] Output cache shared across replicas.…","i":"Session.SessionQuestionAnswers Event.EventQuestionAnswers SessionQuestionViewBuilder CategoryItemLookupService SessionScoringProcessor SpeakerDashboardService SessionQuestionAnswers EventQuestionAnswers SessionCategoryItems SpeakerCategoryItems GetOpenPollsHandler PublicSessionDetail"},{"u":"/docs/governance/adc-RemediationBacklog.html#deliberate--accepted-recorded-decisions-not-scheduled-work","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"Deliberate / accepted (recorded decisions, not scheduled work)","x":"Conscious, recorded choices, not pending work (the former TECHDEBT.md accepted-risk section): - Single-region deployment (no multi-region failover): accepted in…","i":"SessionRoomScheduling.ValidateRoomAssignmentAsync MMCA.ADC.Notification.Application ConstructorDependencyCountTests LocalizedTextConventionTests TranslationCompletenessTests ArchitecturalAnalysis.md PseudoLocalizationTests AuthenticationService BrandColorTokenTests DeviceSettings.razor skip_freshness_gates SliceCohesionTests"},{"u":"/docs/governance/adc-RemediationBacklog.html#already-at-level-4-protect-dont-regress","d":"MMCA.ADC: Architecture Remediation Backlog","k":"Architecture Governance","t":"✅ Already at level 4: protect, don't regress","x":"1 SOLID · 2 Design Patterns · 3 Clean Architecture · 4 Domain-Driven Design · 5 Vertical Slice Architecture · 6 CQRS & Event-Driven · 7 Microservices Readiness · 8 Data…","i":"AnthropicScoringService.ScoreSessionAsync MMCA.ADC.CrossService.IntegrationTests GetSessionSelectionDashboardHandler SessionSelectionDashboard.razor.cs GetSpeakerSessionOverlapHandler GetCategoryDistributionHandler Session.AddSessionCategoryItem Session.CategoryItem.Duplicate Speaker.AddSpeakerCategoryItem Speaker.CategoryItem.Duplicate ObservabilityConventionTests OperationCanceledException"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html","d":"Architecture Evaluation Criteria","k":"Architecture Governance","x":"A structured rubric for evaluating the architecture of an enterprise application. Each category defines what is being assessed, concrete criteria to check, red flags that signal…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#how-to-use-this-rubric","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"How to Use This Rubric","x":"Score each category 0–4. Use the same scale everywhere so totals are comparable. Alongside the maturity level, rate how well each category is actually implemented on a finer 0–10…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#1-solid-principles","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"1. SOLID Principles","x":"Intent: Object/module-level design discipline that keeps code flexible and decoupled. Criteria - SRP: each class/handler has one reason to change; no \"god\" services orchestrating…","i":"NotSupportedException switch new"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#2-design-patterns","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"2. Design Patterns","x":"Intent: Appropriate, idiomatic use of patterns, solving real problems, not pattern theater. Criteria - Creational (Factory methods on entities, Builder, Options) used where…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#3-clean-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"3. Clean Architecture","x":"Intent: Dependencies point inward; business rules are independent of frameworks, UI, and data stores. Criteria - Dependency rule enforced: Domain → (nothing); Application →…","i":"JsonProperty DbContext Table"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#4-domain-driven-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"4. Domain-Driven Design","x":"Intent: The model reflects the business; boundaries follow capability boundaries, not technical layers. Criteria - Ubiquitous language: type/method names match business terms…","i":"decimal Result string Guid"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#5-vertical-slice-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"5. Vertical Slice Architecture","x":"Intent: Code is organized by feature/capability, so a change touches one cohesive slice. Criteria - Features grouped by use case (command/query + handler + validator + DTO…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#6-cqrs--event-driven-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"6. CQRS & Event-Driven Design","x":"Intent: Reads and writes are separated where it pays off; integration via events is reliable. Criteria - Commands (mutate, return Result) and queries (read, side-effect-free) are…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#7-microservices-readiness","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"7. Microservices Readiness","x":"Intent: Whether services (or future-extractable modules) are independently deployable and own their data. Criteria - Service boundaries align with bounded contexts; one team can…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#8-data-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"8. Data Architecture","x":"Intent: Persistence, consistency, and migrations are deliberate and safe. Criteria - Transaction boundaries match aggregate boundaries; unit-of-work scope is clear. - Migrations…","i":"Include"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#9-api--contract-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"9. API & Contract Design","x":"Intent: External and inter-service contracts are clear, stable, and evolvable. Criteria - Consistent resource/endpoint design (REST/minimal APIs/gRPC) with predictable shapes. -…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#10-cross-cutting-concerns","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"10. Cross-Cutting Concerns","x":"Intent: Validation, caching, resilience, configuration, and mapping are centralized and consistent. Criteria - Validation, logging, transactions handled by pipeline…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#11-security","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"11. Security","x":"Intent: AuthN/AuthZ, secrets, and data protection are correct by construction. Criteria - Authentication centralized; tokens validated; identity flows documented (e.g.,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#12-performance--scalability","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"12. Performance & Scalability","x":"Intent: The system meets latency/throughput goals and scales horizontally. Criteria - Async I/O throughout; no sync-over-async; no blocking the request thread. - Hot-path query…","i":"Result Wait"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#13-observability--operability","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"13. Observability & Operability","x":"Intent: You can understand and operate the system in production. Criteria - Structured logging with correlation/trace IDs flowing across module/service boundaries. - Distributed…","i":"Console.WriteLine"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#14-testability--test-strategy","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"14. Testability & Test Strategy","x":"Intent: The design supports fast, reliable, meaningful tests at the right levels. Criteria - Healthy test pyramid: many fast unit tests on domain/application, fewer integration,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#15-best-practices--code-quality","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"15. Best Practices & Code Quality","x":"Intent: Day-to-day craftsmanship that keeps the codebase healthy. Criteria - Analyzers at error severity (style, security, threading, maintainability) enforced in CI;…","i":"disable warning pragma"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#16-maintainability--evolvability","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"16. Maintainability & Evolvability","x":"Intent: The system absorbs change cheaply and ages well. (The governance/documentation depth behind this (ADRs, fitness functions, diagrams) is scored separately in §34.)…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#17-devops--deployment","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"17. DevOps & Deployment","x":"Intent: Building, releasing, and provisioning are automated, repeatable, and safe. (The local developer experience / inner loop behind this (local orchestration, cross-repo dev,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#18-ui-architecture--component-design","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"18. UI Architecture & Component Design","x":"Intent: Components are cohesive, reusable, and composed cleanly, the UI has a deliberate structure, not page-sized blobs. Criteria - Container/presentational split: smart…","i":"EventCallback ShouldRender razor key"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#19-state-management--data-flow","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"19. State Management & Data Flow","x":"Intent: Client state has a clear owner and predictable flow; server state is cached and invalidated deliberately. Criteria - Single source of truth per piece of state; ownership…","i":"StateHasChanged IsDirty"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#20-design-system-theming--ui-consistency","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"20. Design System, Theming & UI Consistency","x":"Intent: A coherent visual language enforced by a component library, not re-implemented per screen. Criteria - Component library used consistently (e.g., MudBlazor): teams build…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#21-accessibility-a11y","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"21. Accessibility (a11y)","x":"Intent: The UI is usable by everyone, including assistive-technology users, and ideally enforced, not aspirational. Criteria - Semantic structure: correct…","i":"span div"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#22-responsive-design--cross-browserdevice","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"22. Responsive Design & Cross-Browser/Device","x":"Intent: The UI works across viewport sizes, input modes, and supported browsers. Criteria - Fluid/responsive layouts via the design system's grid/breakpoints; no fixed-width…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#23-front-end-performance--rendering","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"23. Front-End Performance & Rendering","x":"Intent: The UI loads and responds fast; rendering work is bounded. (Complements §12: this is the client side.) Criteria - Initial load: bundle/payload size controlled;…","i":"ShouldRender key"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#24-forms-validation--ux-safety","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"24. Forms, Validation & UX Safety","x":"Intent: Data entry is safe, forgiving, and consistent, users don't lose work or get confused by errors. Criteria - Validation parity: client-side validation for fast feedback…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#25-navigation-routing--information-architecture","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"25. Navigation, Routing & Information Architecture","x":"Intent: Users can find their way; routes are meaningful, guarded, and role-aware. Criteria - Route design: clean, bookmarkable, deep-linkable URLs; parameters typed and…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#26-front-end-security","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"26. Front-End Security","x":"Intent: The client doesn't become the weak link, XSS, token handling, and trust boundaries are correct. (Complements §11.) Criteria - Output encoding / XSS: no unsanitized HTML…","i":"MarkupString innerHTML"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#27-internationalization--localization","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"27. Internationalization & Localization","x":"Intent: The UI can be translated and respects culture, if in scope. (Score weight 0–1 if single-locale by design.) Criteria - Externalized strings: UI text in resource files, not…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#28-front-end-testing--quality","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"28. Front-End Testing & Quality","x":"Intent: The UI is verified at the right levels with stable, meaningful tests. (Complements §14.) Criteria - Component tests (e.g., bUnit) for rendering logic, parameters, events,…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#29-resilience-reliability--business-continuity","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"29. Resilience, Reliability & Business Continuity","x":"Intent: The system survives partial failure and recovers from disaster within defined objectives. (Extends the resilience facets of §7/§12 into a first-class recovery story.)…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#30-compliance-privacy--data-governance","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"30. Compliance, Privacy & Data Governance","x":"Intent: Personal and regulated data is classified, governed, and handled lawfully across its lifecycle. (§11 defends against attackers; this answers to regulators.) Criteria -…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#31-cost-efficiency--finops","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"31. Cost Efficiency / FinOps","x":"Intent: Cloud spend is proportional to value and driven by data, not guesswork. (§17 mentions cost; this makes it a first-class axis.) Criteria - Right-sizing: compute/database…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#32-dependency--supply-chain-management","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"32. Dependency & Supply-Chain Management","x":"Intent: Third-party and inter-package dependencies are controlled, auditable, and evolve safely, especially critical for a framework that publishes packages. (Elevates §15's…"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#33-developer-experience--inner-loop","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"33. Developer Experience & Inner Loop","x":"Intent: Developers build, run, test, and iterate locally with fast, low-friction feedback. (Promoted out of §17: that scores release/ops automation; this scores the inner loop.)…","i":"editorconfig local.props"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#34-architecture-governance--documentation","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"34. Architecture Governance & Documentation","x":"Intent: Decisions are recorded, conformance is enforced, and the system is documented so it stays coherent as it evolves. (Promoted out of §16: that scores the property of…","i":"CLAUDE.md"},{"u":"/docs/governance/ArchitectureEvaluationCriteria.html#appendix-quick-scan-checklist","d":"Architecture Evaluation Criteria","k":"Architecture Governance","t":"Appendix: Quick-Scan Checklist","x":"A 2-minute triage before the full evaluation: any \"no\" warrants a deeper look. - [ ] Can you draw the dependency graph and is it acyclic and inward-pointing? - [ ] Is the domain…"},{"u":"/docs/governance/common-ArchitectureScorecard.html","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","x":"Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.Common's architecture scores (replaces the former single-axis snapshot; see git…","i":"ResilienceCircuitBreakerFaultInjectionTests SpecificationsDoNotNavigateToOtherEntities CrossSourceSpecification.BuildAsync BaseIntegrationEvent.SchemaVersion AggregateRootEntityControllerBase ArchitectureEvaluationCriteria.md DomainInvariantViolationException LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture OpenIdConnectMetadataWarmupTask ResourceTranslationsAreComplete EventVersioningConventionTests"},{"u":"/docs/governance/common-ArchitectureScorecard.html#scorecard","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Scorecard","x":"Weighted column = Maturity·weight / Implementation·weight per row. Axis-gap findings: §17/§8 are mature-but-execution-deferred (mechanism shipped, deeper proof lives downstream);…","i":"SpecificationsDoNotNavigateToOtherEntities CrossSourceSpecification.BuildAsync BaseIntegrationEvent.SchemaVersion TransportDoesNotLeakIntoCoreLayers ArchitectureEvaluationCriteria.md CrossDataSourceDegradeConvention MMCA.Common.Testing.Architecture OpenIdConnectMetadataWarmupTask EventVersioningConventionTests ListPageQueryStateServiceTests PermissionAuthorizationHandler PiiErasureContractFitnessTests"},{"u":"/docs/governance/common-ArchitectureScorecard.html#indices","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Indices","x":"- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 316 ÷ 324 = 97.5% (up from 96.9% (314/324) on the targeted 2026-08-22 update: §9 API & Contract Design Maturity 3→4 on…","i":"ServiceContract"},{"u":"/docs/governance/common-ArchitectureScorecard.html#top-5-strengths","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 strengths","x":"1. Dual-enforced Clean Architecture dependency rule (compile-time + fitness functions), §3 (impl 9): Source/Build/MMCA.Common.LayerEnforcement.targets:1-90 fails the build on…","i":"BaseIntegrationEvent.SchemaVersion MMCA.Common.Testing.Architecture EventVersioningConventionTests ResolveProjectReferences packages.lock.json FixedTimeEquals Result.Failure BeforeTargets Theory Fact"},{"u":"/docs/governance/common-ArchitectureScorecard.html#top-5-risks","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 risks","x":"Note (twenty-first wave, v1.121.0): earlier waves closed risks previously listed here (§29's restore drill, the §27 i18n train, §24 forms enforcement, §22's firefox gate, §23's…","i":"PiiErasureContractFitnessTests ServiceContractPurityTestsBase OutboxPollFilterProcessor NavigationContractTests required_status_checks PiiConventionTests CONTRIBUTING.md ServiceContract DEPLOYMENT.md IAnonymizable PiiRedactor COST.md"},{"u":"/docs/governance/common-ArchitectureScorecard.html#cross-repo-comparison","d":"MMCA.Common: Architecture Scorecard","k":"Architecture Governance","t":"Cross-repo comparison","x":"How this repo's quality relates to the other consumers (where the framework's quality propagates, where a repo diverges or under-uses it, and where a consumer does better than…"},{"u":"/docs/governance/common-RemediationBacklog.html","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","x":"Derived from ArchitectureScorecard.md (canonical two-axis scoring: Maturity 97.5% / Implementation 84.8%, framework v1.152.0. Targeted update 2026-08-22: 9 API & Contract Design…","i":"ArchitectureScorecard.md required_status_checks RedisDistributedLock IDistributedLock BenchmarkDotNet IsDirtyAccessor ServiceContract Performance baseline c911480 f292233 NoWarn"},{"u":"/docs/governance/common-RemediationBacklog.html#implementation-band-implementation--8-ranked-by-implpriority","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Implementation band (implementation <= 8, ranked by implPriority)","x":"Added 2026-07-28 when the ledger gained its second ranked axis. Until then implementation gaps were never ranked or scheduled, which is why consecutive steady-state cycles moved…","i":"ArchitecturalAnalysis.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-first-wave-2026-06-08","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: first wave (2026-06-08)","x":"Implemented in MMCA.Common, ✅ verified 2026-06-09: dotnet build -c Release is clean (0 warnings / 0 errors, all analyzers) and all 9 test projects pass (~1,611 tests, 0…","i":"MessageBusSettings.RetryLimit ConfigureBrokerTransport Directory.Packages.props IntegrationEventConsumer RetryMaxIntervalSeconds RetryMinIntervalSeconds DependencyVersionTests OutboxCleanupService UseMessageRetry MobileCardList BunitTestBase IAnonymizable"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-second-wave-2026-06-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: second wave (2026-06-09)","x":"✅ Verified: dotnet build -c Release clean (0/0) and all 9 test projects pass (1,511 tests, 0 failures). - ✅ 32 / 16: supply-chain. NuGet lock files (RestorePackagesWithLockFile,…","i":"RestorePackagesWithLockFile ServiceContractAttribute nuget.config CqrsMetrics WithMetrics AddMeter package Release dotnet snupkg build list"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-third-wave-front-end-2026-06-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: third wave (front-end, 2026-06-09)","x":"✅ Verified: build clean (0/0) and all 9 test projects pass (1,519 tests, 0 failures); UI tests 90 → 98 (8 new bUnit tests). - ✅ 19: UnsavedChangesGuard live-accessor. Added…","i":"Page.AssertNoAccessibilityViolationsAsync Deque.AxeCore.Playwright MobileInfiniteScrollList UnsavedChangesGuard MaxRenderedItems IsDirtyAccessor CurrentIsDirty PageLoading PageHeader Virtualize MMCATheme PageError"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-fourth-wave-breaking-changes--consumer-sweep-2026-06-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: fourth wave (breaking changes + consumer sweep, 2026-06-09)","x":"✅ Verified across all three repos (built/tested via local.props against Common source, no token): Common 1,523, ADC 1,241, Store 1,088 tests, 0 failures; all CI solutions build…","i":"AggregateConventionTests IntegrationEventConsumer UserNotification.Create EntityConventionTests OutboxCleanupService AddInboxMessages UserNotification BaseDomainEvent NoOpInboxStore InboxMessages EfInboxStore IDomainEvent"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1800-2026-06-26","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: v1.80.0 (2026-06-26)","x":"The single-axis backlog above is from the 2026-06-08/09 review (index 80%). The framework has since reached v1.82.0 and the canonical scoring was the in-repo, two-axis…","i":"PermissionAuthorizationHandler BaseDomainEvent.DateOccurred UserNotification.MarkAsRead PermissionRegistryBuilder AddAuthorizationPolicies ArchitectureScorecard.md GlobalRateLimitPartition PermissionPolicyProvider RateLimitPartitionTests RoleNames.ContentEditor UserNotification.ReadOn IPermissionRegistry"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1810v1820--governance-pass-2026-06-26","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: v1.81.0/v1.82.0 + governance pass (2026-06-26)","x":"Released since v1.80.0 (v1.81.0, v1.82.0) plus a sixth governance pass currently in flight (uncommitted). All of it lands in categories already scored 9-10, so the two-axis…","i":"ArchitectureEvaluationCriteria.md MMCA.Common.Aspire.Security SecurityHeadersMiddleware AddCommonSecurityHeaders ICspPolicyProvider MapCommonScalarUi Scalar.AspNetCore ValidAlgorithms RsaSha256 FACTS.md b9a6a28 COST.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1830v1840-2026-06-27","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: v1.83.0/v1.84.0 (2026-06-27)","x":"Released since v1.82.0 (v1.83.0, v1.84.0) plus a docs-only governance pass currently in flight (uncommitted). One score moved at this wave: §30 Implementation 7→8. The canonical…","i":"OpenIdConnectMetadataWarmupTask INotificationRecipientProvider ArchitectureScorecard.md IPushNotificationSender WarmupHostedService WarmupReadinessGate AddServiceDefaults PiiConventionTests PiiRedactorTests UserNotification IWarmupTask PiiRedactor"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1850-eighth-wave-under-8-implementation-remediation-2026-06-27","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress, v1.85.0 (eighth wave: under-8 Implementation remediation, 2026-06-27)","x":"The under-8 Implementation remediation (commit 78e5312, tag v1.85.0, HEAD 7082a5f) lifted every category scored Implementation one maturity score. Re-verified against current…","i":"MMCA.Common.Testing.Architecture ArchitectureRules.Slices.cs PasswordComplexityAttribute ArchitectureScorecard.md AuthModelValidationTests DataAnnotationsValidator ServiceContractAttribute TraceIdRatioBasedSampler SliceCohesionTestsBase ParentBasedSampler SliceCohesionTests NavigationFlow.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-v1860v1920-ninth-wave-i18n--re-score-2026-06-29","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress, v1.86.0→v1.92.0 (ninth wave: i18n + re-score, 2026-06-29)","x":"Re-scored against current source at framework v1.92.0 (HEAD 93ffcac, dirty tree). Canonical scoring is now Maturity 91.7% / Implementation 84.1% (was 92.8% / 85.0%) per the…","i":"PiiErasureContractFitnessTests WebApplicationExtensions.cs ArchitectureScorecard.md ConfigureBrokerTransport IntegrationEventConsumer User.PreferredCulture UseDelayedRedelivery cfg.UseMessageRetry PiiConventionTests DataSubjectSample PasswordHasher.cs IStringLocalizer"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-tenth-wave-focused-in-repo-remediation-2026-06-30","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: tenth wave (focused in-repo remediation, 2026-06-30)","x":"Four scores moved up on shipped, tested in-repo evidence; both indices rose for the first time in several waves: Maturity 91.7% → 92.9% (301/324), Implementation 84.1% → 84.9%…","i":"MMCA.Common.Testing.Architecture PaletteDark.PrimaryContrastText ResourceTranslationsAreComplete DatabaseRestoreDrillTests LocalizationResourceTests Directory.Packages.props PrimitivesSnapshotTests SupportedCultures.All PaletteDark.Primary WarningContrastText ErrorContrastText ACCESSIBILITY.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-eleventh-wave-adr-governance-2026-06-30","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: eleventh wave (ADR governance, 2026-06-30)","x":"No score moves. A full 34-category evidence re-score at framework v1.93.0 (HEAD 3e72bfa, dirty tree) re-confirmed every category at its tenth-wave value; indices hold at Maturity…","i":"AggregateRootEntityControllerBase EntityControllerBase OwnerOrAdminFilter OwnershipHelper Specification customer_id FACTS.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-twelfth-wave-under-8-implementation-lift-v1940-pending-2026-06-30","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: twelfth wave (under-8 Implementation lift, v1.94.0 pending, 2026-06-30)","x":"Two Implementation scores move up, Maturity holds: Implementation 84.9% → 85.3% (691/810), Maturity 92.9% (301/324) unchanged. Full Release build clean, 1685 tests pass. Held for…","i":"LocalizedTextConventionTestsBase ListPageQueryStateServiceTests SupportedCultures.PseudoLocale LocalizedTextConventionTests PseudoStringLocalizerFactory UseCommonRequestLocalization PseudoLocalizationE2ETests ListPageStateServiceTests LocalizationResourceTests PseudoLocalizer.Transform IStringLocalizerFactory PseudoLocalizationTests"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---fourteenth-wave-clean-tree-evidence-re-score-at-v11010-2026-07-03","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - fourteenth wave (clean-tree evidence re-score at v1.101.0, 2026-07-03)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.101.0 (HEAD 5e55be2, working tree clean: the recurring…","i":"ArchitectureScorecard.md FormsConventionTestsBase RegisterFormTests.cs Testing.Architecture Scalar.AspNetCore ValidationMessage FACTS.md slnx"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---defect-fix-wave-c-1c-7-2026-07-05","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - defect-fix wave C-1..C-7 (2026-07-05)","x":"Seven approved defect fixes, each behavior change landed with its pinning test flipped (or a new regression test) in the same change; build 0/0 and the full .slnx suite green.…","i":"Microsoft.Extensions.TimeProvider.Testing EntityServiceBase.GetAllForLookupAsync SessionCookieAuthenticationHandler OAuthControllerBase.CompleteAsync AuthenticatedServiceBase ChildEntityServiceBase LoginProtectionService LoggingQueryDecorator ITokenStorageService KeyNotFoundException OutboxCleanupService Uri.EscapeDataString"},{"u":"/docs/governance/common-RemediationBacklog.html#progress-sixteenth-wave-clean-tree-re-score-at-v11060-2026-07-06","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress: sixteenth wave (clean-tree re-score at v1.106.0, 2026-07-06)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.106.0 (HEAD 6f8b917, one commit past the v1.106.0 tag, working tree…","i":"ArchitecturalAnalysis.md ArchitectureScorecard.md Directory.Packages.props EncryptedStringConverter SECURITY.md FACTS.md b75fa8f Theory Fact"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---seventeenth-wave-evidence-re-score-at-v11080-2026-07-09","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - seventeenth wave (evidence re-score at v1.108.0, 2026-07-09)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.108.0 (git HEAD 6c3b3bc, working tree clean, one commit ahead of…","i":"ILiveChannelPublisher ACCESSIBILITY.md FACTS.md ci.yml"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---eighteenth-wave-runtime-performance-wave-2026-07-10","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - eighteenth wave (runtime performance wave, 2026-07-10)","x":"A cross-repo runtime-performance audit (4 parallel auditors: framework, ADC, Store, hosting/config) found the framework strong on read-path fundamentals (no-tracking, SQL…","i":"PublicEndpointOutputCachePolicy EFReadRepository.ApplyIncludes PooledConnectionLifetime HttpResilienceDefaults CachingQueryDecorator LocalView.FindEntry ExecuteUpdateAsync InProcessEventBus AllowAnonymous DetectChanges ExpandoObject CHANGELOG.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---remediation-wave-1-cross-repo-wave-plan-2026-07-11","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - remediation wave 1 (cross-repo wave plan, 2026-07-11)","x":"First wave of the 2026-07-11 cross-repo remediation plan (workspace plan file). Ships the shared §18/§19 fitness bases the ADC/Store maturity lifts need, closes the tenth-wave 20…","i":"StateManagementConventionTestsBase UIArchitectureConventionTestsBase ErrorMessages._localizer MobileInfiniteScrollList AllowedStaticMembers PrimaryContrastText WebVitalsCollector ErrorContrastText WebVitalsE2ETests DarkModeE2ETests NotificationBell CHANGELOG.md"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---eighteenth-wave-evidence-re-score-at-v11150-2026-07-12","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - eighteenth wave (evidence re-score at v1.115.0, 2026-07-12)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.115.0 (HEAD 37d0a3b, working tree clean, at the release tag). Three…","i":"ArchitectureScorecard.md MMCA.Common.UI.Maui PrimaryContrastText ErrorContrastText WebVitalsE2ETests DarkModeE2ETests MudDataGrid FACTS.md rgba"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twentieth-wave-evidence-re-score-at-v11170-2026-07-17","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twentieth wave (evidence re-score at v1.117.0, 2026-07-17)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.117.0 (HEAD 76d70cf, working tree clean). Four scores move.…","i":"ArchitectureScorecard.md NavigationContractTests required_status_checks AuthorizeAttribute NavigationFlow.md MMCA.Common.UI RouteAttribute RESPONSIVE.md FACTS.md bicep build Short"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-first-wave-evidence-re-score-at-v11210-2026-07-21","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-first wave (evidence re-score at v1.121.0, 2026-07-21)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.121.0 (HEAD 4a4fc05, working tree clean). One score moves.…","i":"ArchitectureScorecard.md required_status_checks BenchmarkDotNet CONTRIBUTING.md Notifications Performance baseline FACTS.md COST.md verify Short gate"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-second-wave-evidence-re-score-at-v11230-2026-07-23","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-second wave (evidence re-score at v1.123.0, 2026-07-23)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.123.0 (HEAD c911480, working tree clean). No score moves. Canonical…","i":"UnsavedChangesGuard.IsDirtyAccessor PiiErasureContractFitnessTests PasswordComplexityAttribute IIntegrationEventPublisher ArchitectureScorecard.md OpenApiContractTestsBase IConnectionMultiplexer EntityQueryPipeline IEventBus EditForm FACTS.md c911480"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-third-wave-evidence-re-score-at-v11280-2026-07-25","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-third wave (evidence re-score at v1.128.0, 2026-07-25)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.128.0 (HEAD 3dff29b, working tree clean). No score moves, the third…","i":"ArchitectureScorecard.md WebVitalsE2ETests ICommandHandler IQueryHandler pull_request permissions Unreleased FACTS.md TResult ci.yml github Result"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-fourth-wave-evidence-re-score-at-v11310-2026-07-28","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-fourth wave (evidence re-score at v1.131.0, 2026-07-28)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.131.0 (HEAD 2c52aa9, working tree clean). No score moves, the…","i":"ArchitectureScorecard.md OpenApiContractTestsBase AddCommonApiVersioning MMCA.Common.UI.Maui ICommandHandler ServiceContract AllowAnonymous AllowAnyOrigin IQueryHandler FACTS.md TResult CS1591"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-fifth-wave-evidence-re-score-at-v11350-2026-08-01","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-fifth wave (evidence re-score at v1.135.0, 2026-08-01)","x":"A full 34-category, two-pass evidence re-score (per-category scorer plus adversarial verifier) at framework v1.135.0 (HEAD f292233, working tree clean). One score moves, ending…","i":"EntityQueryService.GetAllForLookupAsync DomainInvariantViolationException ArchitectureScorecard.md InProcessDistributedLock HttpResilienceDefaults IConnectionMultiplexer RedisDistributedLock NuGetAuditSuppress IdempotencyFilter IDistributedLock v1.128.0..HEAD AddCaching"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---twenty-sixth-wave-evidence-re-score-at-v11420-2026-08-07","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - twenty-sixth wave (evidence re-score at v1.142.0, 2026-08-07)","x":"Full 34-category two-pass re-score at HEAD 710d29d (clean tree). No scores move: 27 categories re-confirmed fresh, and seven first-pass lift proposals were refuted on the…","i":"GetAllForLookupAsync packages.lock.json AddMeter FACTS.md orderBy OrderBy secrets l.Name navbar NoWarn where"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---security-invariants-wave-11-hardening-2026-08-22","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - security invariants wave (§11 hardening, 2026-08-22)","x":"Closes the two §11 gaps surfaced by the Article 16 (JWKS dual-fetch) review: the insecure dev defaults that no two-axis entry named as scheduled work, and the absent security…","i":"AnonymousEndpointTestsBase AddForwardedJwtBearer RequireHttpsMetadata RsaJwksProvider AllowAnonymous false main true"},{"u":"/docs/governance/common-RemediationBacklog.html#progress---9-contract-surface-gates-2026-08-22","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Progress - §9 contract-surface gates (2026-08-22)","x":"Closes both halves of 9, the last weight-2 Maturity-3 item that had a named in-repo lever. Landed via MMCA.Common PR 271 (squash 8a6c603, merged 2026-08-22). - ✅ OpenAPI…","i":"ServiceContractsDoNotDependOnServiceInternals ServiceContractPurityTestsBase OpenApiBaselineTests AddCommonOpenApi MapCommonOpenApi ServiceContract ProblemDetails FACTS.md"},{"u":"/docs/governance/common-RemediationBacklog.html#deferred---2026-07-19-full-review-recorded-not-scheduled","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Deferred - 2026-07-19 full review (recorded, not scheduled)","x":"The 2026-07-19 full framework review shipped its accepted fixes on the review branch (rollback on business failure + post-commit dispatch, outbox leases + dead-letter visibility,…","i":"MMCA.Common.Infrastructure MMCA.Common.UI.Tests MMCA.Common.UI.Maui IServiceCollection IMessageBus LangVersion extension IsFailure preview TResult CS1591 NoWarn"},{"u":"/docs/governance/common-RemediationBacklog.html#recorded---2026-07-31-consumer-discovered-defect-not-scheduled","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"Recorded - 2026-07-31 consumer-discovered defect (not scheduled)","x":"Found downstream while implementing MMCA.ADC BR-239 (public speaker visibility), which needed a filtered lookup read. Recorded rather than fixed in place: the consumer already…","i":"EntityQueryService.GetAllForLookupAsync MMCA.Common.Shared.ValueObjects.Email IRepository.GetAllForLookupAsync QueryFieldService.Validate InvalidOperationException GetOrBuildLookupSelector BaseLookup.Name nameProperty asTracking ToString orderBy OrderBy"},{"u":"/docs/governance/common-RemediationBacklog.html#priority-6-highest-leverage","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔴 Priority 6: highest leverage","x":"The package ships reusable Blazor primitives with no fast test tier. - ~~(medium) No component tests for the UI library~~ RESOLVED: Tests/Presentation/MMCA.Common.UI.Tests…","i":"Page.AssertNoAccessibilityViolationsAsync PiiErasureContractFitnessTests AuditableBaseEntity.Delete Deque.AxeCore.Playwright EncryptedStringConverter MobileInfiniteScrollList MMCA.Common.Testing.E2E MMCA.Common.Testing.UI MMCA.Common.UI.Tests OutboxCleanupService UnsavedChangesGuard DeleteConfirmation"},{"u":"/docs/governance/common-RemediationBacklog.html#priority-3-score-3-weight-3-one-rung-from-a-4","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Priority 3: score 3, weight 3 (one rung from a 4)","x":"- ~~(medium) No broker retry policy on the extracted-microservice path~~ RESOLVED (re-verified 2026-06-29): ConfigureBrokerTransport applies cfg.UseMessageRetry (exponential) on…","i":"DomainAggregateRootsHaveNoPublicConstructors ResilienceCircuitBreakerFaultInjectionTests Add_DifferentCurrencies_ReturnsFailure HandleBeforeInternalNavigationAsync MobileInfiniteScrollListTests.cs AggregateRootsHaveResultFactory MessageBusSettings.RetryLimit AggregateConventionTestsBase DomainExposesAggregateRoots DomainFactoriesReturnResult RestorePackagesWithLockFile UnsavedChangesGuardTests.cs"},{"u":"/docs/governance/common-RemediationBacklog.html#priority-2-score-3-weight-2-polish--hardening","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟡 Priority 2: score 3, weight 2 (polish / hardening)","x":"- (medium) No consumer-side idempotency/inbox for at-least-once broker delivery: duplicate side effects possible in any non-idempotent consumer. (low) ~~Same misleading…","i":"ServiceContractsDoNotDependOnServiceInternals EntityQueryPipeline.MaxUnboundedResultLimit ApplicationSettings.MaxPageSize MessageBusSettings.EnableInbox ServiceContractPurityTestsBase ArchitectureRules.Slices.cs MobileInfiniteScrollList OpenApiContractTestsBase ServiceContractAttribute Directory.Build.targets AddCommonApiVersioning required_status_checks"},{"u":"/docs/governance/common-RemediationBacklog.html#already-at-level-4-protect-dont-regress","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"✅ Already at level 4: protect, don't regress","x":"1 SOLID · 2 Design Patterns · 3 Clean Architecture · 5 Vertical Slice (maturity 3→4 on the slice-cohesion fitness function) · 7 Microservices Readiness · 8 Data Architecture · 10…","i":"MessageBusSettings.EnableInbox NavigationContractTests IConnectionMultiplexer required_status_checks WebVitalsE2ETests IDistributedLock BenchmarkDotNet ServiceContract EditorRequired Performance baseline navbar"},{"u":"/docs/governance/common-RemediationBacklog.html#deliberate--accepted-documented-caps-not-scheduled-work","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔒 Deliberate / accepted (documented caps, not scheduled work)","x":"Moved out of the active priority queue on 2026-07-02 (user-approved). Its computed priority = (4 − 2) × 2 = 4 is the highest weighted gap of any open category, but the unmet §31…","i":"NavigationFlow.md ACCESSIBILITY.md CONTRIBUTING.md NUGET_API_KEY RESILIENCE.md RESPONSIVE.md CHANGELOG.md release.yml SECURITY.md main.bicep CLAUDE.md README.md"},{"u":"/docs/governance/common-RemediationBacklog.html#mostly-consumer-assessed-the-shared-commonui-surface-is-scored-here","d":"MMCA.Common: Architecture Remediation Backlog","k":"Architecture Governance","t":"⚪ Mostly consumer-assessed (the shared Common.UI surface is scored here)","x":"21 Accessibility · 26 Front-End Security (Assessable mainly in consumer apps; 26 shared surface is covered under 11.) - 22 Responsive: CLOSED at Maturity 4 / Implementation 9…","i":"LocalizedTextConventionTests PseudoLocalizationE2ETests AuthModelValidationTests NavigationContractTests PasswordComplexity NavigationFlow.md RegisterFormTests ValidationMessage ResxMudLocalizer Forbidden EditForm slnx"},{"u":"/docs/governance/store-ArchitectureScorecard.html","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","x":"Canonical, version-controlled scorecard for this repo: the single source of truth for MMCA.Store's architecture scores. This is Store's first in-repo governance artifact…","i":"CK_InventoryItem_AvailableQuantity_NonNegative ArchitectureEvaluationCriteria.md ConstructorDependencyCountTests StateManagementConventionTests UIArchitectureConventionTests ObservabilityConventionTests MobileInfiniteScrollList PseudoLocalizationTests GracefulShutdownTests Money.ToDisplayString RemediationBacklog.md ServiceInfoController"},{"u":"/docs/governance/store-ArchitectureScorecard.html#executive-summary","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Executive summary","x":"MMCA.Store is a .NET 10.0 (LangVersion preview) DDD/Clean Architecture e-commerce system (Catalog, Sales, Identity modules; Stripe checkout) extracted into independently-hosted…","i":"MMCA.Common.Testing.Architecture IntegrationEventContractTests LocalizedTextConventionTests TreatWarningsAsErrors DataResidencyTests dbo.OutboxMessages PiiConventionTests Store_Identity Store_Catalog Store_Sales MMCAStore"},{"u":"/docs/governance/store-ArchitectureScorecard.html#scorecard","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Scorecard","x":"Weighted = Maturity·weight / Implementation·weight. Axis-gap finding: §21 Accessibility is honestly M3/I8 (the chromium axe gate earns Implementation 8; Maturity caps at 3…","i":"CK_InventoryItem_AvailableQuantity_NonNegative MMCA.Store.CrossService.IntegrationTests FrameworkVersionConsistencyTests IntegrationEventContractTests.cs ConstructorDependencyCountTests StateManagementConventionTests SQLitePCLRaw.bundle_e_sqlite3 UIArchitectureConventionTests CultureInfo.InvariantCulture LocalizedTextConventionTests ObservabilityConventionTests TranslationCompletenessTests"},{"u":"/docs/governance/store-ArchitectureScorecard.html#indices","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Indices","x":"- Maturity index = Σ(maturity×weight) ÷ Σ(weight×4) = 313 ÷ 320 = 97.8% (re-confirmed with no moves on the 2026-08-14 re-score; §12, §21, and §22 are the three categories at…"},{"u":"/docs/governance/store-ArchitectureScorecard.html#top-5-strengths","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 strengths","x":"1. Clean Architecture + DDD + CQRS depth, fitness-enforced: §3/§4 (impl 9): inherited framework substance, guarded by the shared NetArchTest suite (23 test classes,…","i":"GracefulShutdownTests IAnonymizable"},{"u":"/docs/governance/store-ArchitectureScorecard.html#top-5-risks","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Top 5 risks","x":"1. Accessibility maturity is capped pending a human pass: §21 (mat 3, weight 3): the 23-scan axe suite gates the deploy (impl 8), but the rubric pairs axe-in-CI with a recorded…","i":"BrandColorTokenTests FormsConventionTests deploy.needs a1de5a89 MudForm"},{"u":"/docs/governance/store-ArchitectureScorecard.html#cross-repo-comparison","d":"MMCA.Store: Architecture Scorecard","k":"Architecture Governance","t":"Cross-repo comparison","x":"How Store relates to MMCA.Common (the framework) and MMCA.ADC (the sibling consumer) is maintained once, for all three repos, in the workspace-internal…"},{"u":"/docs/governance/store-RemediationBacklog.html","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","x":"Derived from ArchitectureScorecard.md (two-axis: Maturity 97.8% / Implementation 83.9%, full re-score 2026-07-28, re-confirmed with no score moves on the 2026-08-14 full…","i":"ArchitectureScorecard.md"},{"u":"/docs/governance/store-RemediationBacklog.html#priority-a11y--e2e-merge-gate-21-28-22","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🔴 Priority: a11y / E2E merge gate (#21, #28, #22)","x":"The former single biggest maturity lever: 28 cleared 2026-07-03; 22 cleared on the 2026-07-17 re-score (the gate flip verified live) and reopened on the 2026-07-28 re-score when…","i":"github.event_name matrix.browser workflow_call deploy.needs deploy.yml browsers d057afc e2e.yml Theory needs Fact"},{"u":"/docs/governance/store-RemediationBacklog.html#priority-execution-quality-gaps-impl-not-maturity","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Priority: execution-quality gaps (impl, not maturity)","x":"Ranked 2026-07-28 when the ledger gained its second ranked axis. Until then the items in this section were closed history plus two open levers, with no ranking and no inclusion…","i":"MMCA.Store.CrossService.IntegrationTests IdentityModuleDbSeederBase.ShouldSeed SqlServerIntegrationTestFixtureBase CultureInfo.InvariantCulture MobileInfiniteScrollList ProductVariantChanged NotifyStateChanged workflow_dispatch EmailExistsAsync CatalogBrowse GetPagedAsync InventoryItem"},{"u":"/docs/governance/store-RemediationBacklog.html#priority-minor--accept-or-polish","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟡 Priority: minor / accept-or-polish","x":"- [x] 32 Dependency & Supply-Chain, impl 7 → 8. DONE (2026-07-03, drift plan D8 + D9). Vulnerability gate is NuGetAudit + TreatWarningsAsErrors at restore, which fails…","i":"ServiceInfoController TreatWarningsAsErrors BrandColorTokenTests FormsConventionTests CustomerEmailRules NuGetAuditSuppress Store_Identity Store_Catalog Store_Sales ApiVersion Deprecated MMCAStore"},{"u":"/docs/governance/store-RemediationBacklog.html#defect-fix-wave-2026-07-05","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🐞 Defect-fix wave (2026-07-05)","x":"Four reviewed product defects fixed in one wave; every behavior change flipped its pinning test in the same change. - [x] S-1 Stripe network errors escaped the Result pattern.…","i":"Payment.Stripe.SessionRetrievalFailed Payment.Stripe.SessionCreationFailed Payment.Stripe.UnsupportedCurrency CartStateService.InitializeAsync ExportUserDataHandler HttpRequestException StripePaymentService CheckoutAndPayAsync DeleteUserHandler UserRole.IsAdmin CheckoutOutcome UserRole.Admin"},{"u":"/docs/governance/store-RemediationBacklog.html#deliberate--accepted-record-the-choice-dont-silently-leave-low","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"Deliberate / accepted (record the choice; don't silently leave low)","x":"- ADR-042 device capability abstraction (latent, drift plan D8). The core extension point is converged (Store wires browser + MAUI capabilities via…","i":"AddBrowserDeviceCapabilities CultureInfo.InvariantCulture LocalizedTextConventionTests TranslationCompletenessTests UseMauiDeviceCapabilities Money.ToDisplayString ProductVariantChanged MMCA.Common.UI.Maui SliceCohesionTests DeepLinkListener ResxMudLocalizer DeviceUIModule"},{"u":"/docs/governance/store-RemediationBacklog.html#below-maturity-4-tracking-inclusion-policy-categories-scoring--4-maturity","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟠 Below-maturity-4 tracking (inclusion policy: categories scoring < 4 maturity)","x":"These categories score maturity 3; the ledger records them per its own line-4 inclusion policy (mirrors ADC's equivalent entries). - [x] 19 · State Management & Data Flow ·…","i":"StateManagementConventionTestsBase UIArchitectureConventionTestsBase StateManagementConventionTests UIArchitectureConventionTests ProductDetail.razor.cs OrderDetail.razor.cs ProductVariantsPanel StoreArchitectureMap OrderSummaryPanel OrderLinesPanel OPERATIONS.md sloAlertSpecs"},{"u":"/docs/governance/store-RemediationBacklog.html#resolved-this-cycle-2026-07-28-drift-wave-d1d2d5d6d7--e2e4e7e8","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved this cycle (2026-07-28, drift wave: D1/D2/D5/D6/D7 + E2/E4/E7/E8)","x":"- [x] 29 Resilience: the DR drill was restoring a RETIRED database. The weekly dr-drill.yml had no rotation and fell through to its input default MMCAStore, the legacy archive no…","i":"AuthControllerBase.LoginAsync HandlerResultConventionTests PaymentReconciliationService DecoratorPipelineOrderTests PeriodicBackgroundService AddCommonRateLimiting skip_freshness_gates alertEmailAddress authIpPermitLimit Store_Identity RegisterAsync Store_Catalog"},{"u":"/docs/governance/store-RemediationBacklog.html#resolved-this-cycle-2026-07-25-performance-program-2","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved this cycle (2026-07-25, performance program 2)","x":"Second evidence-led performance pass over Common/ADC/Store. Store's share shipped as two PRs plus the v1.127.0 framework sweep. - [x] Output cache shared across replicas. Catalog…","i":"AddStackExchangeRedisOutputCache Filter.Operator.NotSupported GetVariantCartInfoHandler BulkSetInventoryHandler IProductVariantService GetUnitPricesAsync IDistributedCache IntFilterStrategy OrderLines.Count PaymentInitiated EvictByTagAsync ProductVariants"},{"u":"/docs/governance/store-RemediationBacklog.html#resolved-this-cycle-2026-07-11-drift-convergence-drift-plan-d1-d13","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"🟢 Resolved this cycle (2026-07-11 drift-convergence, drift plan D1-D13)","x":"- [x] 29 DR gates (drift plan D3). dr-freshness is now in deploy.needs (fails a deploy when the last successful dr-drill is stale), dr-drill.yml gained a weekly cron, and…","i":"ConstructorDependencyCountTests MMCA.Store.Gateway.Tests GracefulShutdownTests MMCA.Store.CI.slnf Store_Identity Store_Catalog workflow_call deploy.needs TimeProvider Store_Sales Directory db_owner"},{"u":"/docs/governance/store-RemediationBacklog.html#already-at-level-4-protect-dont-regress","d":"MMCA.Store: Architecture Remediation Backlog","k":"Architecture Governance","t":"✅ Already at level 4 (protect, don't regress)","x":"Both axes satisfied (maturity 4 AND implementation = 9), the true protect list: SOLID (1), Design Patterns (2), Clean Architecture (3), DDD (4), Data (8), API (9), Observability…","i":"CK_InventoryItem_AvailableQuantity_NonNegative FormsConventionTests IQueryable"},{"u":"/docs/guides/index.html","d":"Guides & Specifications","k":"Guides & Specifications","x":"The narrative documentation for the MMCA platform: adoption guides, business specifications, workflow analyses, and per-concern reference notes. Files are prefixed by the repo…"},{"u":"/docs/guides/index.html#framework-mmcacommon","d":"Guides & Specifications","k":"Guides & Specifications","t":"Framework (MMCA.Common)","x":"- Getting Started: stand up a new application from the MMCA.Templates scaffold, in six steps. - Build MMCA.ECommerce: the two-module store sample (Products + Orders) built end to…","i":"MMCA.Templates"},{"u":"/docs/guides/index.html#mmcastore-e-commerce","d":"Guides & Specifications","k":"Guides & Specifications","t":"MMCA.Store (e-commerce)","x":"- Business Specification - Business Workflow Analysis - Navigation Flow - Manual Screen-Reader Pass Runbook"},{"u":"/docs/guides/index.html#mmcaadc-conference","d":"Guides & Specifications","k":"Guides & Specifications","t":"MMCA.ADC (conference)","x":"- Business Specifications - Navigation Flow - Manual Screen-Reader Pass Runbook - Integration-Test Tier Rework Plan Related reading: the Architecture Decision Records and the…"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","x":"The automated layer (axe-core WCAG 2.1 AA scans in Tests/E2E/MMCA.ADC.E2E.Tests/AccessibilityTests.cs plus the shared Login/Register/Profile bases in MMCA.Common.Testing.E2E)…","i":"MMCA.Common.Testing.E2E RemediationBacklog.md"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#tools","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Tools","x":"Run against the app started via Aspire (dotnet run --project Source/Hosting/MMCA.ADC.AppHost), reaching the UI through the Gateway. Test with the keyboard only (no mouse) for the…","i":"dotnet start Ctrl Alt Cmd run"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#what-to-verify-on-every-flow","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"What to verify on every flow","x":"1. Landmarks and headings. The SR's landmark/heading list (NVDA Insert+F7) exposes a logical banner / navigation / main structure and a sensible h1...hN outline (one h1 per…","i":"MainLayout.razor navigation h1...hN banner Insert Shift main Tab"},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#results-log","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Results log","x":"Record one row per pass. A flow with any unresolved blocker is a FAIL (open a backlog item for it)."},{"u":"/docs/guides/adc-ACCESSIBILITY-SCREENREADER-PASS.html#after-a-pass","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"After a pass","x":"- File any defect as a remediation item; cross-link it from the §21 row's evidence. - Update the dated row above so the scorecard can cite a real, current manual pass (this is…"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","x":"Status: complete (Phase 4 broker-transport tier landed 2026-07-06; Phase 5 residual = coverlet only). - Phase 0 ✅: Tests/WebAPI revived as MMCA.Common.API middleware unit tests…","i":"Microsoft.Testing.Extensions.CodeCoverage ISessionBookmarkValidationService IdentityIntegrationTestFixture SpeakerUnlinkedFromUserHandler AnonymousConferenceReadTests SpeakerLinkedToUserHandler MMCA.ADC.Integration.slnf MMCA.ADC.IntegrationTests IIntegrationEventHandler AddForwardedJwtBearer AttendeeBookmarkTests IBookmarkCountService"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#recommended-strategy-two-tiers","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Recommended strategy: two tiers","x":"1. Primary: per-service WebApplicationFactory : one in-process host per service (Identity / Conference / Engagement), cross-service edges mocked. AddBrokerMessaging…","i":"DistributedApplicationTestingBuilder SpeakerUnlinkedFromUser WebApplicationFactory SpeakerLinkedToUser AddBrokerMessaging UserRegistered Program"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#three-code-facts-that-shape-the-rework-verified","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Three code facts that shape the rework (verified)","x":"- Only Conference.Service is WAF-incompatible: it ends with StartAsync() + self-HTTP/2 WarmupViaHttpAsync + WaitForShutdownAsync(). Identity/Engagement/Notification use…","i":"AddCommonAuthentication AddForwardedJwtBearer WebApplicationFactory WaitForShutdownAsync Conference.Service WarmupViaHttpAsync JwtTokenGenerator IssuerSigningKey JwtBearerOptions app.RunAsync StartAsync authority"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#databases","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Databases","x":"- SQLite in-memory for the fast bulk tier (no Docker, CI-friendly; DatabaseInitStrategy=EnsureCreated). - MsSql Testcontainers for a tagged SQL-fidelity subset (soft-delete…","i":"SQLServerDbContext DataSources migrations"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#project-structure","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Project structure","x":"- One WAF test project per service (MMCA.ADC.{Identity,Conference,Engagement}.IntegrationTests): can't reference two Program-bearing hosts in one project. - One…","i":"MMCA.ADC.CrossService.IntegrationTests IntegrationTestBase MMCA.Common.Testing JwtTokenGenerator IntegrationTests ProjectReference MMCA.Common.API WebAPI.Tests Conference Engagement Identity MMCA.ADC"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#ci","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"CI","x":"- Add the SQLite per-service tier to CI.slnf (seconds, no Docker) → restores the authz/CRUD merge gate (11) with no workflow change. - Keep the container-based MsSql + RabbitMQ…","i":"CI.slnf"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#phased-sequencing-fastest-win-first","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Phased sequencing (fastest win first)","x":"- Phase 0: re-home WebAPI.Tests middleware unit tests; drop the dead WebAPI reference; re-add to slnx+CI.slnf. ~16 tests green; removes a non-building project (16). - Phase 1:…","i":"ISessionBookmarkValidationService IBookmarkCountService OwnerOrAdminFilter ServiceTestFixture JwtBearerOptions AttendeeClaims OrganizerUser WebAPI.Tests TProgram CI.slnf slnx"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#key-risks","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Key risks","x":"- The non-Identity JwtBearerOptions in-process override is the trickiest piece: prove it on one Conference auth test before fanning out. - SQLite vs SQL-Server fidelity (owned…","i":"JwtBearerOptions"},{"u":"/docs/guides/adc-IntegrationTestReworkPlan.html#critical-files","d":"Integration-Test Tier Rework Plan (RemediationBacklog #14)","k":"Guides & Specifications","t":"Critical files","x":"- Tests/Integration/MMCA.ADC.IntegrationTests/Infrastructure/TestWebApplicationFactory.cs (combined-host factory → split into per-service fixtures; its JWT config block is the…","i":"AddCommonAuthentication AddForwardedJwtBearer JwtTokenGenerator.cs MMCA.ADC.CI.slnf MMCA.ADC.slnx StartAsync partial Program public class"},{"u":"/docs/guides/adc-NavigationFlow.html","d":"Navigation Flow","k":"Guides & Specifications","x":"This document maps the site navigation flow for each actor in the MMCA.ADC application. Each mermaid diagram shows the pages accessible to that actor and the directional…"},{"u":"/docs/guides/adc-NavigationFlow.html#actors","d":"Navigation Flow","k":"Guides & Specifications","t":"Actors","x":"Roles & menu: Organizer is the only elevated role (default is Attendee). A Speaker is an attendee whose account is linked to a Speaker, surfaced via the speakerid claim. The left…","i":"IUIModule.NavItems speaker_id Organizer Attendee"},{"u":"/docs/guides/adc-NavigationFlow.html#1-anonymous-user","d":"Navigation Flow","k":"Guides & Specifications","t":"1. Anonymous User","x":"Pages accessible without authentication: home, login, register, the two password-reset pages, and all public conference pages. ---"},{"u":"/docs/guides/adc-NavigationFlow.html#2-attendee-authenticated-user","d":"Navigation Flow","k":"Guides & Specifications","t":"2. Attendee (Authenticated User)","x":"Inherits all anonymous pages. Gains access to profile, feedback submission, and session bookmarking. Unauthenticated visitors are redirected to login. ---"},{"u":"/docs/guides/adc-NavigationFlow.html#3-speaker","d":"Navigation Flow","k":"Guides & Specifications","t":"3. Speaker","x":"Inherits all attendee pages. Gains access to the speaker dashboard for managing their own profile, viewing assigned sessions, and reviewing feedback ratings. ---"},{"u":"/docs/guides/adc-NavigationFlow.html#4-organizer","d":"Navigation Flow","k":"Guides & Specifications","t":"4. Organizer","x":"Authenticated users with the Organizer role. Inherits all attendee and public pages. Adds CRUD management for every conference entity (events, sessions, speakers, categories,…","i":"Organizer"},{"u":"/docs/guides/adc-NavigationFlow.html#5-functionality-flows-attendee--speaker","d":"Navigation Flow","k":"Guides & Specifications","t":"5. Functionality Flows (Attendee & Speaker)","x":"The diagrams in sections 1-4 map which pages each actor can reach. The diagrams below map how attendees and speakers accomplish each functionality, including inline actions…","i":"DeviceUIModule speaker_id route"},{"u":"/docs/guides/adc-NavigationFlow.html#navigation-patterns","d":"Navigation Flow","k":"Guides & Specifications","t":"Navigation Patterns","x":"- Unauthenticated users accessing protected pages are redirected to /login via the RedirectToLogin component. - Successful login/register redirects to Home (/) with a full page…","i":"RegisteredUser_AdminPages_ShouldBeForbidden Engagement.CheckIn IUIModule.NavItems Engagement.Points EventList.razor RedirectToLogin DeviceUIModule UserList.razor Routes.razor speaker_id attribute Authorize"},{"u":"/docs/guides/adc-specifications.html","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","x":"---"},{"u":"/docs/guides/adc-specifications.html#1-system-overview","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"1. System Overview","x":"ADC is a conference management system for the Atlanta Developers Conference. It provides backend services to manage multi-day conference events, sessions, speakers, rooms,…"},{"u":"/docs/guides/adc-specifications.html#2-domain-model","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"2. Domain Model","x":"Relationships: - Owns many Rooms (child entities) - Owns many EventSpeakers (child join entities linking Event ↔ Speaker) - Owns many EventQuestionAnswers (child feedback…","i":"Engagement.LivePolls Engagement.SessionQA User.LinkedSpeakerId Event.StartDate Session.EventId ContentEditor Event.EndDate EventSpeaker QuestionType Waitlisted Nominated Organizer"},{"u":"/docs/guides/adc-specifications.html#3-business-rules","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"3. Business Rules","x":"Reading guide: Some rules reference other rules defined later in the document (e.g., BR-63, BR-80 are defined in Section 10). Forward references use the BR- numbering…","i":"Event.QuestionModerationDefault Event.LastSessionizeRefreshBy Event.LastSessionizeRefreshOn SessionQuestionAnswerChanged SpeakerQuestionAnswerChanged EventQuestionAnswerChanged SessionCategoryItemChanged SpeakerCategoryItemChanged UserSessionBookmarkChanged Session.AccessibilityInfo Session.IsServiceSession SessionFeedbackSubmitted"},{"u":"/docs/guides/adc-specifications.html#4-use-cases--business-processes","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"4. Use Cases / Business Processes","x":"See UC-30 (Registration) and UC-31 (Login) in Section 12.2 for the current email + password authentication flows. UC-34 (Request password reset) and UC-35 (Reset password) in the…","i":"SpeakerQuestionAnswersController Event.LastSessionizeRefreshBy Event.LastSessionizeRefreshOn SpeakerQuestionAnswerChanged Engagement.LivePolls Engagement.SessionQA UserSessionBookmark skippedSoftDeleted IsServiceSession IsPlenumSession AllowAnonymous QuestionEntity"},{"u":"/docs/guides/adc-specifications.html#5-workflows--state-transitions","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"5. Workflows & State Transitions","x":"The Session.Status field is a free-text string imported from Sessionize. Default: null (for manually created sessions). Known Sessionize values: Accepted, Waitlisted, Accept…","i":"Session.Status ContentEditor IsConfirmed IsInformed Waitlisted Nominated Organizer Accepted Declined Decline Accept Queue"},{"u":"/docs/guides/adc-specifications.html#6-events--side-effects","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"6. Events & Side Effects","x":"Domain events are raised for entity mutations. Not all events have registered handlers: events without handlers serve as extension points for future requirements. Note: Only…","i":"SessionQuestionAnswerChanged SpeakerQuestionAnswerChanged EventQuestionAnswerChanged SessionCategoryItemChanged SpeakerCategoryItemChanged UserSessionBookmarkChanged SessionSpeakerChanged User.LinkedSpeakerId CategoryItemChanged EventSpeakerChanged UserPasswordChanged CategoryChanged"},{"u":"/docs/guides/adc-specifications.html#7-business-constraints--invariants","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"7. Business Constraints & Invariants","x":"---","i":"Speaker.LinkedUserId User.LinkedSpeakerId IsServiceSession Session.EventId QuestionEntity ContentEditor EventSpeaker nameProperty Waitlisted CreatedBy FirstName Nominated"},{"u":"/docs/guides/adc-specifications.html#8-external-integrations-business-perspective","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"8. External Integrations (Business Perspective)","x":"---","i":"Speaker.ProfilePicture Event.VenueMapUrl IsServiceSession IsPlenumSession QuestionSource SessionizeCode IsTopSpeaker RecordingUrl LiveUrl POST"},{"u":"/docs/guides/adc-specifications.html#9-glossary","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"9. Glossary","x":"---","i":"Event.IsPublished IsServiceSession IsPlenumSession ContentEditor IsTopSpeaker Organizer User.Role Admin Role true"},{"u":"/docs/guides/adc-specifications.html#ddd-structural-summary","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"DDD Structural Summary","x":"Why three bounded contexts instead of two: The original Events + Identity split grouped all conference-related entities together regardless of write profile. Separating…","i":"SessionQuestionAnswer SpeakerQuestionAnswer EventQuestionAnswer Question.IsRequired Session.EventId QuestionEntity SpeakerChanged ContentEditor QuestionType Room.EventId RoomChanged Organizer"},{"u":"/docs/guides/adc-specifications.html#10-specification-clarifications--addenda","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"10. Specification Clarifications & Addenda","x":"This section addresses gaps, ambiguities, and implicit design decisions identified during implementation review. New business rules are numbered BR-61+. API contract…","i":"SessionQuestionAnswersController SpeakerQuestionAnswersController TimeZoneInfo.ConvertTimeFromUtc EventQuestionAnswersController MMCA.ADC.Modules.Engagement RemoveSpeakerQuestionAnswer UpdateSpeakerQuestionAnswer AddSpeakerQuestionAnswer SessionFeedbackSubmitted EventFeedbackSubmitted CreateQuestionHandler SessionQuestionAnswer"},{"u":"/docs/guides/adc-specifications.html#11-api-contract-specifications","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"11. API Contract Specifications","x":"This section documents API design decisions that apply across all endpoints. --- All error responses use the RFC 9457 ProblemDetails format (the successor to RFC 7807, same…","i":"SessionQuestionAnswer SpeakerQuestionAnswer EventQuestionAnswer PaginationMetadata Session.Duration Speaker.FullName DomainException includeChildren FirstRowOnPage LastModifiedOn QuestionEntity TotalPageCount"},{"u":"/docs/guides/adc-specifications.html#12-authentication--identity-architecture","d":"ADC (Atlanta Developers Conference) - Business Specifications","k":"Guides & Specifications","t":"12. Authentication & Identity Architecture","x":"This section defines the authentication mechanism for both the Web UI (Blazor) and MAUI (mobile) clients, which share a common Razor class library. It replaces the device-based…","i":"CascadingAuthenticationState AuthenticationStateProvider PasswordReset__ResetUrl PasswordResetController Auth.InvalidResetToken MaxValidationAttempts RequestWindowMinutes Speaker.LinkedUserId TokenLifetimeMinutes User.LinkedSpeakerId MaxRequestsPerEmail UserPasswordChanged"},{"u":"/docs/guides/common-ACCESSIBILITY.html","d":"Accessibility (rubric §21)","k":"Guides & Specifications","x":"The shared MMCA.Common.UI surface targets WCAG 2.1 AA. Accessibility is enforced two ways: an automated axe-core gate in CI (the bulk of coverage) and a documented manual…","i":"MMCA.Common.UI"},{"u":"/docs/guides/common-ACCESSIBILITY.html#automated-coverage-axe-core-wcag-21-aa","d":"Accessibility (rubric §21)","k":"Guides & Specifications","t":"Automated coverage (axe-core, WCAG 2.1 AA)","x":"The ui-e2e CI job runs Playwright + axe-core against the backend-less gallery; chromium is the blocking merge gate (firefox/webkit advisory). Scanned states: Component render is…","i":"PrimitivesSnapshotTests RegisterPageE2ETests PrimaryContrastText ErrorContrastText DarkModeE2ETests PageLoadingState MMCA.Common.UI progressbar mmca_theme div"},{"u":"/docs/guides/common-ACCESSIBILITY.html#manual-screen-reader-pass","d":"Accessibility (rubric §21)","k":"Guides & Specifications","t":"Manual screen-reader pass","x":"Automation cannot judge reading order, focus management, or announcement quality, so the shared surface is walked manually. Checklist (re-run on any change to MainLayout, the…","i":"ValidationMessage MainLayout.razor PageLoadingState MainLayout EditForm main"},{"u":"/docs/guides/common-ACCESSIBILITY.html#known-limitations-tracked","d":"Accessibility (rubric §21)","k":"Guides & Specifications","t":"Known limitations (tracked)","x":"- ~~Dark-mode contrast (§20, not §21).~~ RESOLVED (2026-07-11). The two dark-palette WCAG AA contrast failures the prototype scan flagged (filled-primary button label ~2.65:1 on…","i":"PaletteDark.PrimaryContrastText WarningContrastText ErrorContrastText DarkModeE2ETests EF5350 rgba"},{"u":"/docs/guides/common-BUILD-BY-HAND.html","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","x":"This is the long-form walkthrough: every project, every file, and every load-bearing line that goes into an application on the MMCA.Common framework, in the order you would…","i":"Contoso.Support Tickets dotnet Orders Order new"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#what-you-will-build","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"What you will build","x":"A modular monolith with one business module and two hosts: - Orders (your business module): an Order aggregate with OrderComment children, opened through a Result-returning…","i":"AllowAnonymous OrderComment Result Order sql web"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-0-prerequisites-and-decisions","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 0: Prerequisites and decisions","x":"Install: - .NET 10 SDK (the framework targets net10.0 with LangVersion: preview for C extension types). - SQL Server reachable locally (LocalDB, a container, or the one Aspire…","i":"FrameworkVersionConsistencyTestsBase Directory.Packages.props MMCA.Common.API UseLocalMMCA LangVersion local.props install net10.0 package preview CS0103 dotnet"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-1-create-the-solution-and-the-build-plumbing","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 1: Create the solution and the build plumbing","x":"Scaffolded. dotnet new mmca-app writes every file in this phase. Read it to know what each one does; you do not need to type any of it. The plumbing files are the load-bearing,…","i":"Directory.Packages.props Directory.Build.props Contoso.Support.slnx local.props.template OrderIdentifierType PackageReference MMCA.Helpdesk auditSources editorconfig nuget.config global.json Contracts"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-2-scaffold-the-module-project-set","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 2: Scaffold the module project set","x":"Scaffolded. dotnet new mmca-app creates this project set for your first module, and pwsh build/add-module.ps1 adds another one later: it drives dotnet new mmca-module and then…","i":"Contoso.Support.Orders.Infrastructure Contoso.Support.Orders.Application Contoso.Support.Orders.Domain Contoso.Support.Orders.Shared Contoso.Support.Orders.API MMCA.Common.Infrastructure MMCA.Common.Application MMCA.Common.Domain MMCA.Common.Shared AddErrorResources MMCA.Common.API AllowAnonymous"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-3-the-vertical-slice-end-to-end-the-heart-of-it","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 3: The vertical slice end-to-end (the heart of it)","x":"Scaffolded. The generated module already contains this slice and six more, worked end to end. dotnet new mmca-command and dotnet new mmca-query add another one. This phase is the…","i":"EntityTypeConfigurationSQLServer ConcurrencyConventionTestsBase AddModuleOrdersInfrastructure ScanModuleApplicationServices AuditableAggregateRootEntity OrderOpenedIntegrationEvent OrderCommentIdentifierType IUnitOfWork.GetRepository AddApplicationDecorators IIntegrationEventHandler DomainEventDispatcher EntityControllerBase"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-4-dbcontext-model-and-migrations","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 4: DbContext model and migrations","x":"Partly scaffolded. The migrations project and its design-time factory are generated. Running dotnet ef migrations add InitialCreate is still yours, and for a module added later…","i":"ApplicationSettings.DatabaseInitStrategy InitializeDatabaseAsync SQLServerDbContext EnsureCreated InitialCreate DataSources migrations Migrate dotnet None add"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-5-compose-the-monolith-host-and-run-it","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 5: Compose the monolith host and run it","x":"Scaffolded. Both hosts, the AppHost, and the .resx pairs are generated. Read this phase before you touch any of them: the DI sequence, WaitFor(sql) rather than the database…","i":"MmcaCultureBootstrap.SetBrowserCultureAsync LocalizedTextConventionTestsBase LocalizationResourceTestsBase UseCommonRequestLocalization OrderOpenedIntegrationEvent UseCommonMiddlewarePipeline services.AddErrorResources AddApplicationDecorators YourModuleErrorResources EnsureSuccessStatusCode EndpointCultureApplier UseRequestLocalization"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-6-tests-and-the-architecture-fitness-map","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 6: Tests and the architecture-fitness map","x":"Scaffolded, with one deliberate gap. All three test projects and the map are generated. The IntegrationEventContractTests subclass is NOT: its frozen literal lists members…","i":"FrameworkVersionConsistencyTestsBase ConstructorDependencyCountTestsBase IntegrationEventContractTestsBase LocalizedTextConventionTestsBase MMCA.Common.Testing.Architecture SpecificationConventionTestsBase MicroserviceExtractionTestsBase ConcurrencyConventionTestsBase ControllerConventionTestsBase IntegrationEventContractTests LocalizationResourceTestsBase HandlerConventionTestsBase"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-7-upgrading-the-framework-version","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 7: Upgrading the framework version","x":"Not scaffolded. dotnet new mmca-app --framework-version picks the version you START on; moving to a later one is this phase. When a new MMCA.Common release ships, upgrade in one…","i":"FrameworkVersionConsistencyTestsBase Directory.Packages.props packages.lock.json UseLocalMMCA local.props your.slnx restore dotnet new"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#phase-8-extract-a-module-into-its-own-service-the-payoff","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Phase 8: Extract a module into its own service (the payoff)","x":"Not scaffolded. The generated solution carries the plumbing (the .Contracts proto convention and the .Service OpenAPI block in Directory.Build.props), but the extraction itself…","i":"GrpcResultExceptionInterceptor OrderOpenedIntegrationEvent MMCA.Common.Aspire.Hosting WithSQLServerDataSource AddGrpcServiceDefaults Directory.Build.props RequestVersionExact AddTypedGrpcClient WithJwksDiscovery MMCA.Common.Grpc Support_Identity OutboxMessages"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#verification-checklist","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Verification checklist","x":"1. Build green: dotnet build Contoso.Support.slnx with no warnings (TreatWarningsAsErrors + five analyzers). This is the primary automatable gate. 2. Unit + architecture tests…","i":"OrderOpenedIntegrationEvent Contoso.Support.slnx IArchitectureMap OutboxMessages InitialCreate OrderComment migrations AppHost dotnet build Order test"},{"u":"/docs/guides/common-BUILD-BY-HAND.html#where-to-look-next","d":"Building on MMCA.Common by Hand","k":"Guides & Specifications","t":"Where to look next","x":"- Getting Started: the one-command path that writes phases 1 through 6 for you. If you are starting a new solution rather than adding the framework to an existing one, that is…","i":"CLAUDE.md README.md Helpdesk Tickets Ticket"},{"u":"/docs/guides/common-COST.html","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","x":"MMCA.Common is a library, so it cannot provision anything: right-sizing, scale rules, budgets, and per-service cost attribution live in the consumer apps' IaC (e.g. MMCA.ADC's…"},{"u":"/docs/guides/common-COST.html#what-the-framework-does-for-cost","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"What the framework does for cost","x":"- Telemetry ingestion is the real line item, so high-volume / low-value spans are dropped. OutboxPollFilterProcessor (MMCA.Common.Aspire) suppresses the recurring OutboxPoll…","i":"http.client.open_connections OutboxPollFilterProcessor TraceIdRatioBasedSampler ConfigureOpenTelemetry OutboxCleanupService AddServiceDefaults MMCA.Common.Aspire ParentBasedSampler SocketsHttpHandler request.duration active_requests AppDependencies"},{"u":"/docs/guides/common-COST.html#recommended-consumer-defaults-set-these-downstream","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"Recommended consumer defaults (set these downstream)","x":"- Telemetry retention & sampling. Tune Log Analytics retention to the minimum the consumer's compliance window allows, and set Telemetry:TracesSampleRatio (the built-in…"},{"u":"/docs/guides/common-COST.html#cost-attribution--guardrail-samples-distilled-from-mmcaadc","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"Cost-attribution & guardrail samples (distilled from MMCA.ADC)","x":"These belong in the consumer's IaC, not the library, but the framework documents the shape so every consumer attributes spend and guards surges the same way. The worked, deployed…"},{"u":"/docs/guides/common-COST.html#out-of-scope-for-the-framework-by-design","d":"Cost & FinOps Notes (rubric §31)","k":"Guides & Specifications","t":"Out of scope for the framework (by design)","x":"Provisioning, scale rules, budgets, per-service cost attribution, and surge/revert automation are consumer/IaC concerns and are not added to the library: see also ADR-009…"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","x":"MMCA.ECommerce is the simplest e-commerce application on the MMCA.Common framework: a Products catalog module and an Orders module with line items, behind a REST API host and a…","i":"MMCA.Templates dotnet new"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#before-you-start","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"Before you start","x":"- .NET 10 SDK (the framework targets net10.0 with LangVersion: preview). - Docker Desktop (Aspire provisions SQL Server as a container). - EF Core tools: dotnet tool install…","i":"MMCA.Templates LangVersion install net10.0 preview dotnet pwsh tool ps1"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#2-generate-the-solution-with-the-products-module","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"2. Generate the solution with the Products module","x":"Five options do most of this guide's old work. Three remove an axis a catalog product does not have, and the code for an axis you turn off is never generated: --flat drops the…","i":"ProductCreatedIntegrationEvent ProductCreatedHandler RequesterUserId Created Opened Name"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#3-add-the-orders-module","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"3. Add the Orders module","x":"build/add-module.ps1 ships inside the solution you just generated. It runs dotnet new mmca-module with the shape options passed through, then performs every wire-up the template…","i":"ECommerceArchitectureMap.cs SQLServerMigrationsAssembly services.AddErrorResources OrderItemIdentifierType WithSQLServerDataSource Directory.Build.props OrdersErrorResources MMCA.ECommerce.slnx ChangeItemQuantity ECommerce_Products appsettings.json ProjectReference"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#4-reshape-products-into-a-catalog-product","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"4. Reshape Products into a catalog product","x":"The scaffolded module arrives as the template's worked example in your namespaces, already shaped by the flags in step 2: no children, no status, no requester, Name instead of…","i":"UpdateRequestsAreConcurrencyAware Microsoft.EntityFrameworkCore Product.Description.TooLong ModuleApplicationDbContext ProductCreateRequestMapper DomainEntityState.Updated Product.Description.Empty Directory.Packages.props DependencyInjection.cs TreatWarningsAsErrors Product.InvalidPrice Product.Name.TooLong"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#5-reshape-orders-into-an-order-with-line-items","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"5. Reshape Orders into an order with line items","x":"Orders keeps the child-collection pattern the template scaffolded, retargeted. -Child Item already did the naming (the entity is OrderItem, the slices are AddItem / EditItem /…","i":"UpdateRequestsAreConcurrencyAware Order.Item.ProductName.TooLong Total_ExcludesSoftDeletedItems EnsureStatusAllowsItemChanges Microsoft.EntityFrameworkCore Order.InvalidStatusTransition Order.Item.ProductName.Empty ChangeOrderStatusRequest.cs OrderPlacedIntegrationEvent ModuleApplicationDbContext Order.CustomerName.TooLong ChangeItemQuantityCommand"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#6-point-the-ui-at-the-new-domain","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"6. Point the UI at the new domain","x":"The scaffolded Blazor host already has the load-bearing parts: the typed ECommerceApiClient calling the API server-side through Aspire service discovery (no CORS, no token), the…","i":"MMCA.ECommerce.Orders.Shared string.IsNullOrWhiteSpace Snackbar.RequiredFields Dialog.Delete.Heading System.Globalization GetProductsAsync ProjectReference missingRequired SectionHeading PageHeading es.resx _field"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#7-create-the-migrations","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"7. Create the migrations","x":"Neither module has a migration yet: any shape flag makes mmca-app drop the template's sample one (it described the sample shape), and -SkipMigration deferred the Orders one to…","i":"editorconfig migrations Migrations dotnet add"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#8-the-two-one-time-fixups-then-run-it","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"8. The two one-time fixups, then run it","x":"Apply the two fixups the scaffold deliberately leaves to you (they are name-dependent, so no generated value could be right). First, sort the using directives and the identifier…","i":"ProductCreatedIntegrationEvent IntegrationEventContractTests OrderPlacedIntegrationEvent ArchitectureTests.cs AllowAnonymous editorconfig SCAFFOLD IDE0021 SA1210 SA1211 DELTA Open"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#verification-checklist","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"Verification checklist","x":"1. Baseline green immediately after mmca-app, before any edit: 81 tests. 2. After build/add-module.ps1: still green at 99 tests, both modules' scaffolded suites running. 3. After…","i":"MMCA.ECommerce.slnx OutboxMessages InitialCreate dotnet build test"},{"u":"/docs/guides/common-ECOMMERCE-SAMPLE.html#where-to-look-next","d":"Build MMCA.ECommerce: a Two-Module Store from the Templates","k":"Guides & Specifications","t":"Where to look next","x":"- MMCA.ECommerce: the finished result of this guide, build- and test-verified. - Getting started: the single-module path, the vertical-slice templates (mmca-command /…"},{"u":"/docs/guides/common-GETTING-STARTED.html","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","x":"MMCA.Common is a .NET 10 framework for DDD, Clean Architecture, and CQRS, shipped as a set of lockstep-versioned NuGet packages (the authoritative list and count live in…"},{"u":"/docs/guides/common-GETTING-STARTED.html#before-you-start","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Before you start","x":"- .NET 10 SDK. The framework targets net10.0 with LangVersion: preview for C extension types. - Docker Desktop. Aspire provisions SQL Server as a container, so you do not install…","i":"MMCA.Templates LangVersion install net10.0 preview dotnet tool"},{"u":"/docs/guides/common-GETTING-STARTED.html#1-install-the-template-pack","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"1. Install the template pack","x":"Four templates arrive: mmca-app (a whole solution), mmca-module (a business module across all five layers), and mmca-command / mmca-query (a single vertical slice)."},{"u":"/docs/guides/common-GETTING-STARTED.html#2-generate-the-solution","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"2. Generate the solution","x":"Three names, and they are independent: the solution (also your root namespace), the first module in plural PascalCase, and that module's aggregate root in singular PascalCase.…","i":"ProjectReference local.props Billing Invoice"},{"u":"/docs/guides/common-GETTING-STARTED.html#3-build-and-test-before-you-change-anything","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"3. Build and test before you change anything","x":"That is a warning-free build with TreatWarningsAsErrors and all five analyzers at error severity, and a passing test run including the architecture-fitness rules, with no…","i":"TreatWarningsAsErrors"},{"u":"/docs/guides/common-GETTING-STARTED.html#4-create-the-first-migration","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"4. Create the first migration","x":"The scaffold ships the migrations project and its design-time factory; the migration itself describes your entities, so it is yours to generate: Always pass --context…","i":"SQLServerDbContext DbSet"},{"u":"/docs/guides/common-GETTING-STARTED.html#5-run-it","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"5. Run it","x":"Run this from a real, interactive terminal. Launched from a headless or background shell the Aspire AppHost stalls at control-plane init and no dashboard appears. The dashboard…","i":"OrderOpenedIntegrationEvent AllowAnonymous POST GET sql web"},{"u":"/docs/guides/common-GETTING-STARTED.html#6-the-two-one-time-fixups","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"6. The two one-time fixups","x":"The scaffold deliberately does not hand these over, because renaming invalidates them and no fixed value is right for every name you could pick. Both are covered in full in the…","i":"IntegrationEventContractTestsBase Contoso.Support.Orders.Shared Zeta.App.Orders.Shared ArchitectureTests.cs editorconfig SCAFFOLD IDE0021 SA1211 Ticket DELTA using"},{"u":"/docs/guides/common-GETTING-STARTED.html#what-you-were-handed","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"What you were handed","x":"The Order aggregate arrives fully worked: a Result-returning factory, invariants, guarded mutations raising domain events, a child entity, soft-delete cascade, the caching pair,…","i":"AddApplicationDecorators Directory.Build.props OrderIdentifierType IArchitectureMap HandleFailure ModuleLoader ErrorType WaitFor global Result DbSet Order"},{"u":"/docs/guides/common-GETTING-STARTED.html#add-your-next-feature","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Add your next feature","x":"A vertical slice (the path every feature follows) is one command, run from the module's UseCases folder: Handlers, validators, and mappers are convention-scanned, so there is no…","i":"order.TransferToRequester AddErrorResources RequesterUserId AddDomainEvent Result.Combine ChangeStatus GetByIdAsync SaveChanges definition IsFailure CacheKey Comments"},{"u":"/docs/guides/common-GETTING-STARTED.html#surface-the-slice-at-the-edge","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Surface the slice at the edge","x":"The scaffold stops at the handler, and the template's closing instructions tell you to map the command in your module's controller. Every write in the generated app follows the…","i":"ThrowIfDomainExceptionAsync _transferRequesterUserId ChangeOrderStatusRequest Api.TransferOrderAsync EntityControllerBase TransferOrderCommand OrderDetail.es.resx ICacheInvalidating ChangeStatusAsync OrderDetail.razor OrderDetail.resx SupportApiClient"},{"u":"/docs/guides/common-GETTING-STARTED.html#then-what","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Then what","x":"- Upgrade the framework. Bump every MMCA.Common. entry in Directory.Packages.props together, in one pass. See Phase 7 and the versioning policy. - Add real authentication. Copy…","i":"Directory.Packages.props Authorize Contracts Service"},{"u":"/docs/guides/common-GETTING-STARTED.html#verification-checklist","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Verification checklist","x":"1. dotnet new mmca-app -n produced a solution that builds and tests green before you changed anything. 2. dotnet build .slnx is warning-free (TreatWarningsAsErrors + five…","i":"OutboxMessages InitialCreate migrations healthy YourApp dotnet build slnx test then add new"},{"u":"/docs/guides/common-GETTING-STARTED.html#where-to-look-next","d":"Getting Started: Build a New App on MMCA.Common","k":"Guides & Specifications","t":"Where to look next","x":"- Templates: every parameter of all four templates, dropping the Blazor UI host, and how the pack is built. ADR-065 explains why it is derived from the reference app rather than…"},{"u":"/docs/guides/common-RESILIENCE.html","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","x":"MMCA.Common is a library, so it cannot operate a deployment: restores, RTO/RPO, and SLO alerting are executed in the consumer apps' IaC (e.g. MMCA.ADC's…"},{"u":"/docs/guides/common-RESILIENCE.html#what-the-framework-provides-and-verifies-in-repo","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","t":"What the framework provides (and verifies in-repo)","x":"Failure isolation, graceful degradation, graceful startup, and the restore procedure itself are therefore demonstrated and tested centrally: the framework drills backup→restore…","i":"ResilienceCircuitBreakerFaultInjectionTests OpenIdConnectMetadataWarmupTask WarmupReadinessHealthCheckTests AddStandardResilienceHandler ConfigureHttpClientDefaults WarmupReadinessHealthCheck DatabaseRestoreDrillTests ConfigureBrokerTransport WarmupHostedServiceTests WarmupReadinessGateTests ResilienceHandlerTests WarmupHostedService"},{"u":"/docs/guides/common-RESILIENCE.html#baseline-slo--error-budget-template-consumers-fill-in","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","t":"Baseline SLO / error-budget template (consumers fill in)","x":"Adopt and tune per app; ADC's filled-in version lives in infra/DISASTER-RECOVERY.md + the SLO metric-alerts in infra/main.bicep. Define RTO/RPO per service (ADC's worked…","i":"requests"},{"u":"/docs/guides/common-RESILIENCE.html#restore-drill-runbook-reference","d":"Resilience & Business Continuity (rubric §29)","k":"Guides & Specifications","t":"Restore-drill runbook (reference)","x":"The only evidence backups actually restore is a periodic drill: restore a throwaway copy, confirm it comes back Online, record the measured restore time, then delete the copy.…"},{"u":"/docs/guides/common-RESPONSIVE.html","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","x":"This document is the supported-device and browser matrix for the shared MMCA.Common.UI component library. It makes the responsive contract explicit (the rubric §22 note that it…","i":"MMCA.Common.UI"},{"u":"/docs/guides/common-RESPONSIVE.html#breakpoints","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Breakpoints","x":"The framework keeps C viewport detection and CSS media queries aligned around one mobile threshold. The C 960px mobile cutoff and the CSS 1023.98px cutoff intentionally differ:…","i":"BreakpointConstants.IsMobileBreakpoint media i.e"},{"u":"/docs/guides/common-RESPONSIVE.html#touch-targets","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Touch targets","x":"Interactive controls on mobile surfaces meet a 48px minimum hit area (Material Design), exceeding both WCAG 2.5.8 Target Size (Minimum, AA, 24px) and WCAG 2.5.5 Target Size…"},{"u":"/docs/guides/common-RESPONSIVE.html#grid-density","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Grid density","x":"DataGridListPageBase exposes a DenseGrid property and a ToggleDensity() method. Derived list pages bind Dense=\"@DenseGrid\" on their MudDataGrid and surface a toggle. The chosen…","i":"ListPageQueryStateServiceTests ListPageStateServiceTests DataGridListPageBase ToggleDensity MudDataGrid DenseGrid TDto"},{"u":"/docs/guides/common-RESPONSIVE.html#browser-matrix","d":"Responsive Design & Cross-Browser Support (rubric §22)","k":"Guides & Specifications","t":"Browser matrix","x":"The shared UI is tested against three Playwright engines in CI (.github/workflows/ci.yml, ui-e2e job): a real-browser axe (WCAG 2.1 AA) + render smoke against the backend-less…","i":"false"},{"u":"/docs/guides/common-TEMPLATES.html","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","x":"MMCA.Templates is a dotnet new pack that scaffolds solutions, modules, and vertical slices on the MMCA.Common framework. It exists because standing up a new app by hand meant 12…","i":"MMCA.Templates UseCases dotnet new"},{"u":"/docs/guides/common-TEMPLATES.html#mmca-app","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"mmca-app","x":"The module and aggregate names are independent, so --module Billing --aggregate Invoice is fine. Everything derived from them follows: routes, the Aspire database resource, the…","i":"IntegrationEventContractTestsBase Contoso.Support.Orders.Shared ShipmentLineIdentifierType Zeta.App.Orders.Shared ArchitectureTests.cs builder.AddProject ProjectReference Contoso.Support EditLineRequest RequesterUserId AddLineRequest AppHost.csproj"},{"u":"/docs/guides/common-TEMPLATES.html#mmca-module","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"mmca-module","x":"All six behave exactly as they do for mmca-app, and they are per module: a solution can hold a flat, status-less catalog module beside one whose aggregate owns a growing child…","i":"SQLServerMigrationsAssembly services.AddErrorResources Architecture.Tests.csproj OrderItemIdentifierType Directory.Build.props Migrations.SqlServer Contoso.Support ErrorResources ModuleLoader DataSources FirstModule RemoveItem"},{"u":"/docs/guides/common-TEMPLATES.html#buildadd-moduleps1","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"build/add-module.ps1","x":"Since 1.4.0 every solution mmca-app generates ships this script, and it is the supported way to add a second module. It runs mmca-module with your shape options passed through,…","i":"IntegrationEventContractTests migrations copyOnly dotnet diff Name slnx add git"},{"u":"/docs/guides/common-TEMPLATES.html#mmca-command-and-mmca-query","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"mmca-command and mmca-query","x":"Run these from the module's UseCases folder. Each creates a folder named after the slice holding its two files. --child-collection exists because both handlers load through…","i":"EntityControllerBase MMCA.Templates GetByIdAsync definition CacheKey Comments includes UseCases contain dotnet nameof Result"},{"u":"/docs/guides/common-TEMPLATES.html#how-the-pack-is-built","d":"Templates: scaffolding an MMCA app","k":"Guides & Specifications","t":"How the pack is built","x":"The template content is the MMCA.Helpdesk reference application itself, staged at pack time. There is no second copy of the solution, so the template cannot drift from the app…"},{"u":"/docs/guides/common-VERSIONING.html","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","x":"MMCA.Common publishes fifteen NuGet packages that are versioned and released together as a single unit. They share one version number so a consumer never has to reason about…","i":"MMCA.Common.UI.Maui release.yml"},{"u":"/docs/guides/common-VERSIONING.html#semantic-versioning","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Semantic Versioning","x":"Versions follow SemVer 2.0: MAJOR.MINOR.PATCH: - MAJOR: reserved (see \"Breaking changes within 1.x\" below). - MINOR: new capability, and the channel breaking changes currently…","i":"vMAJOR.MINOR.PATCH MAJOR.MINOR.PATCH v1.51.0"},{"u":"/docs/guides/common-VERSIONING.html#what-counts-as-breaking","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"What counts as breaking","x":"A change is breaking if it is any of: - Removing or renaming a public type/member, or changing a signature. - Changing the meaning of an existing configuration key, or changing a…","i":"Result"},{"u":"/docs/guides/common-VERSIONING.html#breaking-changes-within-1x","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Breaking changes within 1.x","x":"Breaking changes ship as MINOR bumps, not MAJOR ones, and the version number is therefore not a reliable breakage signal on its own. This is deliberate and follows from the…","i":"IIntegrationEventPublisher IntegrationEventPublisher WithSQLServerDataSource WithDataSource IEventBus v1.123.0 v1.79.0"},{"u":"/docs/guides/common-VERSIONING.html#consumer-rollout","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Consumer rollout","x":"Per project convention, framework upgrades are swept across all consumers in one pass: there are no opt-in flags or phased rollouts for a MMCA.Common change. When a release…"},{"u":"/docs/guides/common-VERSIONING.html#deprecation","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Deprecation","x":"There is no [Obsolete] grace period today. Because the lockstep sweep updates every first-party caller in the same change set, a superseded API is removed in the release that…","i":"Obsolete"},{"u":"/docs/guides/common-VERSIONING.html#supply-chain","d":"Versioning & Breaking-Change Policy","k":"Guides & Specifications","t":"Supply chain","x":"- All package versions are centrally pinned (Directory.Packages.props). - NuGet lock files are committed for reproducible restores. - MassTransit is pinned to v8 by policy (v9…","i":"Directory.Packages.props MassTransit"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","x":"The automated layer (axe-core WCAG 2.1 AA scans in Tests/E2E/MMCA.Store.E2E.Tests/Workflows/AccessibilityTests.cs plus the shared Login/Register/Profile bases in…","i":"MMCA.Common.Testing.E2E"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#tools","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Tools","x":"Run against the app started via Aspire (dotnet run --project Source/Hosting/MMCA.Store.AppHost), reaching the Web UI at https://localhost:6002. Test with the keyboard only (no…","i":"dotnet start Ctrl Alt Cmd run"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#what-to-verify-on-every-flow","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"What to verify on every flow","x":"1. Landmarks and headings. The SR's landmark/heading list (NVDA Insert+F7) exposes a logical banner / navigation / main structure and a sensible h1...hN outline (one h1 per…","i":"Wcag21AaExceptMudPagerCombobox MainLayout.razor MMCA.Common.UI navigation h1...hN banner Insert Shift main Tab"},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#results-log","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"Results log","x":"Record one row per pass. A flow with any unresolved blocker is a FAIL (open a backlog item for it)."},{"u":"/docs/guides/store-ACCESSIBILITY-SCREENREADER-PASS.html#after-a-pass","d":"Manual Screen-Reader Pass: Runbook (§21 Accessibility)","k":"Guides & Specifications","t":"After a pass","x":"- File any defect as a remediation item; cross-link it from the §21 row's evidence. - Update the dated row above so the scorecard can cite a real, current manual pass (this is…"},{"u":"/docs/guides/store-BusinessWorkflows.html","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications"},{"u":"/docs/guides/store-BusinessWorkflows.html#workflow-list-summary","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"Workflow List Summary","x":"---","i":"productId variantId imageId DELETE userId POST GET PUT"},{"u":"/docs/guides/store-BusinessWorkflows.html#1-identity-module-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"1. Identity Module Workflows","x":"Entry Point: POST /auth/register, AuthController.RegisterAsync(), AllowAnonymous Execution Path: Business Steps: 1. Validate registration input (email, password, first name, last…","i":"AuthController.RegisterAsync AuthController.LoginAsync User.RefreshTokenExpiry Customer.ChangeAddress CustomerAddressChanged Customer.ChangeEmail CustomerEmailChanged RequireAuthenticated Customer.ChangeName CustomerNameChanged User.RefreshToken CustomerCreated"},{"u":"/docs/guides/store-BusinessWorkflows.html#2-catalog-module-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"2. Catalog Module Workflows","x":"Entry Point: POST /categories, Admin only, [Idempotent] Response: 201 Created with CategoryDTO Entry Point: PUT /categories/{id}/name, Admin only Entry Point: PUT…","i":"CatalogFeatures.ProductImages ProductVariantPriceChanged ProductVariantCartInfoDTO ProductVariantSkuChanged IProductVariantService ProductVariantRemoved ProductNameChanged ParentCategoryId ProductImageData CategoryDeleted ProductImageDTO ProductDeleted"},{"u":"/docs/guides/store-BusinessWorkflows.html#3-sales-module-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"3. Sales Module Workflows","x":"Entry Point: POST /shoppingcarts/{customerId}/shoppingcartitems, Authenticated (owner or admin via OwnerOrAdminFilter) Decision Points: - Product variant doesn't exist - NotFound…","i":"ShoppingCartItemQuantityAdjusted InventoryItem.AvailableQuantity OrderPaymentFailedSagaHandler BulkSetInventoryResultDTO Order.InventoryRestored ProductVariant.NotFound ShoppingCartItemRemoved IProductVariantService ShoppingCartCheckedOut StripePaymentIntentId ShoppingCart.Status ShoppingCartCleared"},{"u":"/docs/guides/store-BusinessWorkflows.html#4-ui-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"4. UI Workflows","x":"The UI provides a complete shopping experience through the CartDrawer component and Blazor pages. The CartDrawer is the only cart UI: there is no dedicated cart page. It is a…","i":"ICartStateService IUIModule OnChange"},{"u":"/docs/guides/store-BusinessWorkflows.html#5-cross-module-interactions","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"5. Cross-Module Interactions","x":"Module dependency: Sales declares a hard dependency on Catalog (RequiresDependencies = true). When Catalog is disabled, a DisabledProductVariantService stub is registered and…","i":"DisabledProductVariantService IProductVariantService UserRegisteredHandler RequiresDependencies GetUnitPricesAsync GetIdBySkuAsync SkuExistsAsync ExistsAsync true"},{"u":"/docs/guides/store-BusinessWorkflows.html#6-external-interactions","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"6. External Interactions","x":"---","i":"StripePaymentService IDbContextFactory SmtpEmailSender"},{"u":"/docs/guides/store-BusinessWorkflows.html#7-cross-cutting-concerns-participating-in-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"7. Cross-Cutting Concerns Participating in Workflows","x":"---","i":"TransactionalCommandDecorator ProfilingCommandDecorator CachingCommandDecorator ProfilingQueryDecorator ICacheInvalidating OwnerOrAdminFilter IdempotencyFilter ITransactional ApiVersion Idempotent"},{"u":"/docs/guides/store-BusinessWorkflows.html#8-end-to-end-customer-journey","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"8. End-to-End Customer Journey","x":"Alternative Flows: - Payment fails - Order status PaymentFailed - customer can retry (create new Stripe session) - Cancel order - Status Cancelled (from PendingPayment,…","i":"StripePaymentIntentId PaymentFailed Cancelled"},{"u":"/docs/guides/store-BusinessWorkflows.html#9-potentially-missing-or-incomplete-workflows","d":"MMCA Business Workflow Analysis","k":"Guides & Specifications","t":"9. Potentially Missing or Incomplete Workflows","x":"--- This document is derived from source code analysis. All workflows, decisions, and behaviors described above are confirmed implementations traceable to the referenced source…","i":"OrderPaymentFailedSagaHandler OrderCancelledSagaHandler MarkAsDelivered SmtpEmailSender User.Deactivate UserDeactivated"},{"u":"/docs/guides/store-NavigationFlow.html","d":"Navigation Flow","k":"Guides & Specifications","x":"This document maps the site navigation flow for each actor in the MMCA.Store application. Each mermaid diagram shows the pages accessible to that actor and the directional…","i":"NavigationFlow.md"},{"u":"/docs/guides/store-NavigationFlow.html#actors","d":"Navigation Flow","k":"Guides & Specifications","t":"Actors","x":"Roles and enforcement: Admin is the only elevated role (registration creates a Customer). The 14 admin pages carry page-level [Authorize(Roles = \"Admin\")], regression-gated in CI…","i":"customer_id Authorize Customer Admin Roles"},{"u":"/docs/guides/store-NavigationFlow.html#1-anonymous-user","d":"Navigation Flow","k":"Guides & Specifications","t":"1. Anonymous User","x":"Pages accessible without authentication: home, login, register, the two password-reset pages, and the public catalog. Add-to-cart on the product detail page sits inside an…","i":"AuthorizeView Authorize"},{"u":"/docs/guides/store-NavigationFlow.html#2-customer-authenticated-user","d":"Navigation Flow","k":"Guides & Specifications","t":"2. Customer (Authenticated User)","x":"Inherits all anonymous pages. Gains the profile page, the cart drawer (a layout component, not a route), checkout, and their own orders. Unauthenticated visitors deep-linking to…","i":"OrphanOrderRecovery Specification Authorize"},{"u":"/docs/guides/store-NavigationFlow.html#3-admin","d":"Navigation Flow","k":"Guides & Specifications","t":"3. Admin","x":"Inherits all customer pages, plus the admin CRUD surfaces for all three modules. Every page below carries [Authorize(Roles = \"Admin\")]; a customer deep-linking to any of them…","i":"Authorize Roles"},{"u":"/docs/guides/store-NavigationFlow.html#authorization-model","d":"Navigation Flow","k":"Guides & Specifications","t":"Authorization Model","x":"Three cooperating layers; the API is always the boundary: 1. Page-level route guards. The 14 admin pages carry [Authorize(Roles = \"Admin\")] and /profile / /orders carry…","i":"OwnershipHelper.GetOwnershipSpecification OwnerOrAdminFilter mmca_auth_access AuthorizeView customer_id Authorize c4adff2 Roles"},{"u":"/docs/guides/store-Specification.html","d":"MMCA Business Specification Document","k":"Guides & Specifications"},{"u":"/docs/guides/store-Specification.html#1-system-overview","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"1. System Overview","x":"MMCA is an e-commerce platform built with .NET 10.0 using DDD and Clean Architecture. The business logic is organized as modules (Catalog, Sales, Identity) that have been…"},{"u":"/docs/guides/store-Specification.html#2-core-business-entities","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"2. Core Business Entities","x":"Description: A classification grouping for products. Supports hierarchical (parent-child) structures for nested categorization (e.g., \"Jewelry\" \"Rings\"). Key Properties:…"},{"u":"/docs/guides/store-Specification.html#3-business-workflows","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"3. Business Workflows","x":"Trigger: A new user submits registration with first name, last name, email, and password. Steps: 1. Validate registration request (email format, password requirements) 2. Verify…","i":"IInventoryAllocationService.DecrementAsync CatalogFeatures.ProductImages payment_intent.payment_failed EventUtility.ConstructEvent IProductImageStorageService checkout.session.completed ProductImageStorageService OrderCancelledSagaHandler checkout.session.expired Order.InventoryRestored Auth.InvalidResetToken IProductVariantService"},{"u":"/docs/guides/store-Specification.html#4-order-status-state-machine","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"4. Order Status State Machine","x":"Cancellable States: PendingPayment, PaymentInitiated, PaymentFailed Manual Payment States: PendingPayment, PaymentInitiated, PaymentFailed Terminal States: Cancelled, Delivered ---"},{"u":"/docs/guides/store-Specification.html#5-business-rules","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"5. Business Rules","x":"---","i":"ForgotPasswordRequestValidator ProductVariantConfiguration.cs ResetPasswordRequestValidator InventoryItemInvariants.cs AdjustInventoryHandler.cs PasswordResetTokenService ShoppingCartInvariants.cs CategoryConfiguration.cs CheckOutDomainService.cs CustomerConfiguration.cs ForgotPasswordHandler.cs UserRegisteredHandler.cs"},{"u":"/docs/guides/store-Specification.html#6-use-cases","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"6. Use Cases","x":"---"},{"u":"/docs/guides/store-Specification.html#7-domain-events-and-state-changes","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"7. Domain Events and State Changes","x":"---"},{"u":"/docs/guides/store-Specification.html#8-external-integrations","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"8. External Integrations","x":"Purpose: Processes online customer payments for orders. Business Impact: Enables the system to collect payments from customers and confirm payment success or failure…","i":"OrderPaymentFailedSagaHandler payment_intent.payment_failed EventUtility.ConstructEvent checkout.session.completed checkout.session.expired OrderPaidHandler Result.Failure StripeSettings WebhookSecret IEmailSender SecretKey"},{"u":"/docs/guides/store-Specification.html#9-authorization-model","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"9. Authorization Model","x":"Ownership Enforcement: The OwnerOrAdminFilter validates that the route parameter id (CustomerIdentifierType) matches the authenticated user's customer ID, or that the user has…","i":"OwnerOrAdminFilter customer_id user_id email POST role iat jti sub"},{"u":"/docs/guides/store-Specification.html#10-cross-module-communication","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"10. Cross-Module Communication","x":"The system enforces strict module boundaries. Modules communicate only through shared interface contracts: Confirmed behaviors: - Sales module cannot directly access Catalog…","i":"DisabledProductVariantService IProductVariantService RequiresDependencies true"},{"u":"/docs/guides/store-Specification.html#11-user-interface","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"11. User Interface","x":"The UI is a Blazor Server + WebAssembly hybrid (InteractiveAuto render mode) using MudBlazor component library. It supports multiple hosting targets: - Web (Server + WASM):…","i":"UIModuleConfiguration.IsModuleEnabled ICartStateService InteractiveAuto configuration moduleName IUIModule Assembly NavItems"},{"u":"/docs/guides/store-Specification.html#12-cross-cutting-infrastructure","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"12. Cross-Cutting Infrastructure","x":"The IdempotencyFilter (applied via [Idempotent] attribute on Create endpoints) caches the first response for a given Idempotency-Key header value for 24 hours. Duplicate requests…","i":"TransactionalCommandDecorator ProfilingCommandDecorator CachingCommandDecorator ProfilingQueryDecorator ICacheInvalidating IDataSourceService IDbContextFactory IdempotencyFilter ITransactional SemaphoreSlim UseDataSource Idempotent"},{"u":"/docs/guides/store-Specification.html#13-testing","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"13. Testing","x":"- Full customer journey: Register - Browse - Add to Cart - Checkout - Admin Pay - Deliver - Order lifecycle: all state transitions including cancellation with inventory…","i":"MMCA.Store.Integration.slnf MMCA.Store.IntegrationTests WebApplicationFactory STORE_TEST_SQL_BASE"},{"u":"/docs/guides/store-Specification.html#14-missing-or-unclear-business-logic","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"14. Missing or Unclear Business Logic","x":"Observation: The SMTP email service infrastructure is implemented, but no domain event handlers trigger email notifications for events like order confirmation, payment receipt,…","i":"InventoryItemsController InventoryItemList MarkAsDelivered User.Deactivate UserDeactivated CategoryId Delivered GetPaged GetById GetAll Lookup Paid"},{"u":"/docs/guides/store-Specification.html#15-seed-data-initial-system-state","d":"MMCA Business Specification Document","k":"Guides & Specifications","t":"15. Seed Data (Initial System State)","x":"The system seeds the following data at startup: Users: - Admin: one seeded administrator account (Admin role, no Customer record; credentials are environment-specific and not…","i":"ExistsAsync"},{"u":"/","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"Senior Software Architect Ivan Ball-llovera Cloud-native enterprise architecture on the Microsoft stack I design and ship production-grade .NET platforms: modular monoliths that…"},{"u":"/","t":"Architecture that earns its keep","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"I am a Senior Software Architect with more than 25 years designing and delivering scalable, cloud-native systems on the Microsoft stack. My focus is Domain-Driven Design, Clean…"},{"u":"/","t":"The MMCA platform","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"A production-grade .NET 10 framework and a set of reference apps that demonstrate modern enterprise architecture end to end. It is built as a modular monolith that extracts…"},{"u":"/","t":"Deep dives on enterprise .NET","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"A long-form series turning the framework's decisions into teachable patterns, every claim grounded in real source. The three most recent: Proof & getting started · No. 50 The…"},{"u":"/","t":"Speaking & giving back","d":"Ivan Ball-llovera · Senior Software Architect","k":"Site","x":"I help run two community-driven Atlanta technology conferences and keep production-grade patterns free and in the open. See talks & community work Organizer & speaker Two Atlanta…"},{"u":"/resume.html","d":"Résumé","k":"Site","x":"Résumé Ivan Ball-llovera Senior Software Architect 25+ years designing and delivering scalable, cloud-native systems on the Microsoft stack: Domain-Driven Design, Clean…"},{"u":"/resume.html","t":"Professional summary","d":"Résumé","k":"Site","x":"Senior Software Architect with 25+ years designing and delivering scalable, cloud-native systems on the Microsoft stack. Deep expertise in Domain-Driven Design, Clean…"},{"u":"/resume.html","t":"Core competencies","d":"Résumé","k":"Site","x":"Architecture & design Domain-Driven Design, Clean Architecture, CQRS, Modular Monolith → Microservices, Event-Driven Architecture, Outbox Pattern, gRPC, API Gateway (YARP),…"},{"u":"/resume.html","t":"Professional experience","d":"Résumé","k":"Site","x":"Senior Software Engineer · Assurant June 2025 – Present · Architect-level scope: platform, security, and cross-team technical decisions Re-architected the AR.com renters quote…"},{"u":"/resume.html","t":"Featured project · MMCA platform","d":"Résumé","k":"Site","x":"Personal / open source · github.com/ivanball/MMCA.Common A production-grade .NET 10 reference platform demonstrating modern enterprise architecture end-to-end. The conference…"},{"u":"/resume.html","t":"Education","d":"Résumé","k":"Site","x":"B.S., Computer Science University of Havana (Faculty of Mathematics), Havana, Cuba (1994 – 1999)"},{"u":"/resume.html","t":"Languages","d":"Résumé","k":"Site","x":"English · Spanish (bilingual)"},{"u":"/resume.html","t":"Certifications","d":"Résumé","k":"Site","x":"✓ Azure Administrator Associate (AZ-104, 2025) ✓ Azure AI Fundamentals (AI-900, 2024) ✓ Azure Data Fundamentals (DP-900, 2021) ✓ Azure Fundamentals (AZ-900, 2021) → In progress…"},{"u":"/resume.html","t":"Professional development","d":"Résumé","k":"Site","x":"Continuously prototypes emerging technologies, with a current focus on Clean Architecture using .NET 10, Blazor, .NET MAUI, and ASP.NET Core Web API, and on AI-assisted…"},{"u":"/platform.html","d":"The MMCA Platform","k":"Site","x":"Featured work · Open source The MMCA platform A production-grade .NET 10 platform that demonstrates modern enterprise architecture end to end: an open-source framework of fifteen…"},{"u":"/platform.html","t":"MMCA.Common","d":"The MMCA Platform","k":"Site","x":"A NuGet package framework for building modular-monolith applications with DDD, Clean Architecture, and CQRS, and the extension points to extract a module into its own…"},{"u":"/platform.html","t":"Three reference applications","d":"The MMCA Platform","k":"Site","x":"The framework is consumed by real apps. Each one exercises the patterns differently, and the conference app ran a live event on Azure. Conference MMCA.ADC A production-deployed…"},{"u":"/platform.html","t":"From one graph, laptop to cloud","d":"The MMCA Platform","k":"Site","x":"Orchestrated locally with .NET Aspire, then deployed to Azure Container Apps from the same declarative model. The .NET Aspire dashboard: services, databases, and the broker as…"},{"u":"/platform.html","t":"Architectural styles the codebase commits to","d":"The MMCA Platform","k":"Site","x":"The recurring ideas every repo is built on, summarized from the onboarding primer's orientation chapter. Each is taught in full at its first concrete appearance in the reference…"},{"u":"/platform.html","t":"A two-axis architecture scorecard","d":"The MMCA Platform","k":"Site","x":"Every repo is scored against a 34-category rubric on two axes, Maturity (how complete the decision is) and Implementation (how well it is realized in code), with evidence and…"},{"u":"/platform.html","t":"Architecture Decision Records","d":"The MMCA Platform","k":"Site","x":"96 ADRs capture the context and trade-offs behind each cross-cutting pattern, so the design is teachable, not tribal knowledge. Every entry below links to the full record. 001…"},{"u":"/platform.html","t":"The reference library","d":"The MMCA Platform","k":"Site","x":"The full architecture documentation, maintained in this site's repository as its canonical home and rendered as browsable pages: every Architecture Decision Record, the…"},{"u":"/platform.html","t":"Use it, read it, or follow along","d":"The MMCA Platform","k":"Site","x":"The framework is Apache 2.0 and published to nuget.org. The fastest way in is the reference app: one module wired end to end through all five layers, with the extraction path…"},{"u":"/platform.html","t":"Get each deep dive by email","d":"The MMCA Platform","k":"Site","x":"One message per article, no digests and no other mail. Email address Subscribe"},{"u":"/writing.html","d":"Writing","k":"Site","x":"Writing Deep dives on enterprise .NET A long-form series that turns the MMCA framework's architecture decisions into teachable patterns, every claim grounded in real source. Read…"},{"u":"/writing.html","t":"Get each deep dive by email","d":"Writing","k":"Site","x":"One message per article, no digests and no other mail. Email address Subscribe"},{"u":"/speaking.html","d":"Speaking & Community","k":"Site","x":"Speaking & community Talks and giving back For more than 20 years I have been an active contributor to the Microsoft developer communities in Atlanta and South Florida: teaching,…"},{"u":"/speaking.html","t":"Recent sessions","d":"Speaking & Community","k":"Site","x":"Atlanta Cloud + AI Conference · 2026 The App You're Using Right Now Building Atlanta Cloud + AI's own platform with Claude in the loop A field report, not a slide deck about…"},{"u":"/speaking.html","t":"Organizing two Atlanta conferences","d":"Speaking & Community","k":"Site","x":"I help convene developers in person, giving the local community direct, no-cost access to expert content on the Microsoft platform. Lead organizer Atlanta Cloud + AI Conference…"},{"u":"/speaking.html","t":"User groups","d":"Speaking & Community","k":"Site","x":"An active participant in Atlanta's Microsoft technology user-group ecosystem, the same community network from which the conferences draw their speakers and attendees. • Atlanta…"},{"u":"/speaking.html","t":"Open source & mentorship","d":"Speaking & Community","k":"Site","x":"My MMCA framework is Apache-2.0 licensed and documented with architecture decision records, so the patterns are not just usable but teachable. I mentor developers one on one,…"},{"u":"/speaking.html","t":"What I speak on","d":"Speaking & Community","k":"Site","x":"Sessions and workshops for conferences, user groups, and teams. Clean Architecture & DDD on .NET Modular monolith → microservices The transactional outbox Database-per-service…"},{"u":"/contact.html","d":"Contact","k":"Site","x":"Contact Let's connect Happy to talk architecture, the MMCA platform, speaking at your conference or user group, or comparing notes on .NET and Azure. The fastest ways to reach…"},{"u":"/contact.html","t":"Three places to start","d":"Contact","k":"Site","x":"Open source The MMCA platform A .NET 10 framework and three reference apps, graded in the open against a 34-category rubric. See the architecture → Writing Deep dives on…"},{"u":"https://medium.com/@ivanball76/the-mmca-series-every-pattern-one-place-28cf2cee7be8","t":"The series index","d":"Article no. 50","k":"Proof & getting started","x":"The full series index and recommended reading order.","e":1},{"u":"https://medium.com/@ivanball76/undo-is-a-feature-saga-compensation-and-the-reconciliation-backstop-fa017f9591b8","t":"Saga compensation and the reconciliation backstop","d":"Article no. 49","k":"Core patterns","x":"Undo as a first-class event handler: give back stock a committed transaction already took, with a periodic sweep as the saga-timeout backstop.","e":1},{"u":"https://medium.com/@ivanball76/observability-by-default-opentelemetry-and-azure-monitor-in-mmca-673c1886e9e0","t":"Observability by default","d":"Article no. 48","k":"Run & extract","x":"A shared OpenTelemetry baseline with CQRS duration metrics, correlation IDs, and outbox-poll span filtering, exported to Azure Monitor.","e":1},{"u":"https://medium.com/@ivanball76/security-headers-and-csp-for-blazor-one-middleware-every-host-af82df95236e","t":"Security headers and CSP for Blazor","d":"Article no. 47","k":"Auth & the edge","x":"One middleware stamps hardened response headers on every host, with the Blazor CSP resolved through a pluggable provider.","e":1},{"u":"https://medium.com/@ivanball76/field-level-encryption-in-ef-core-aes-gcm-for-pii-columns-06ece340ea25","t":"Field-level encryption in EF Core","d":"Article no. 46","k":"Data & persistence","x":"An AES-256-GCM value converter that keeps a PII column ciphertext even for someone who can query the database.","e":1},{"u":"https://medium.com/@ivanball76/feature-flags-in-the-cqrs-pipeline-gate-commands-not-code-e58b9ea8d098","t":"Feature flags in the CQRS pipeline","d":"Article no. 45","k":"Core patterns","x":"Gate commands and queries at the outermost decorator, so a handler never checks a flag and a disabled feature is rejected before any work runs.","e":1},{"u":"https://medium.com/@ivanball76/http-api-versioning-proven-not-just-claimed-2b0381e4b533","t":"HTTP API versioning, proven not just claimed","d":"Article no. 44","k":"Auth & the edge","x":"Header-based versioning introduced without breaking a single caller, plus a fitness contract that proves two live versions coexist.","e":1},{"u":"https://medium.com/@ivanball76/managed-file-storage-uploads-you-dont-have-to-trust-8dfe8bf016bc","t":"Managed file storage: uploads you don't have to trust","d":"Article no. 43","k":"Data & persistence","x":"Attacker-controlled bytes become safe avatars: content sniffing, metadata stripping, re-encoding, and pluggable blob storage.","e":1},{"u":"https://medium.com/@ivanball76/one-blazor-ui-two-hosts-a-device-capability-layer-that-stays-resolvable-everywhere-b85444693161","t":"One Blazor UI, two hosts","d":"Article no. 42","k":"Proof & getting started","x":"The same Blazor components run in a browser and inside a MAUI hybrid app; small per-capability contracts reach native hardware without ever asking 'am I on mobile?'.","e":1},{"u":"https://medium.com/@ivanball76/two-real-apps-on-one-framework-a-conference-platform-and-a-store-12f694d2a361","t":"Two real apps on one framework","d":"Article no. 41","k":"Proof & getting started","x":"A case study: a conference platform and an e-commerce store built on the same kernel.","e":1},{"u":"https://medium.com/@ivanball76/write-your-first-architecture-fitness-test-d4e25e6a4741","t":"Write your first fitness test","d":"Article no. 40","k":"Proof & getting started","x":"Author your first architecture fitness test and watch it fail the build on a violation.","e":1},{"u":"https://medium.com/@ivanball76/scaffold-a-net-modular-monolith-in-one-command-then-build-your-first-module-b10aacd16d33","t":"Build your first module","d":"Article no. 39","k":"Proof & getting started","x":"A hands-on walkthrough of building a new module across all five layers.","e":1},{"u":"https://medium.com/@ivanball76/one-preference-two-switches-shipping-i18n-and-dark-mode-on-a-single-cookie-and-profile-pipeline-f97186038909","t":"i18n and theming on one preference pipeline","d":"Article no. 38","k":"Proof & getting started","x":"A culture choice and a theme choice ride the same cookie, profile column, and login reconciliation: one persistence path, two switches.","e":1},{"u":"https://medium.com/@ivanball76/a-list-page-in-a-few-lines-a-reusable-blazor-ui-framework-with-the-same-discipline-as-the-backend-c66fa16cd561","t":"A reusable Blazor UI framework","d":"Article no. 37","k":"Proof & getting started","x":"A shared Blazor and MudBlazor UI layer with accessibility enforced by axe in CI.","e":1},{"u":"https://medium.com/@ivanball76/soft-delete-vs-the-right-to-erasure-the-gdpr-conflict-and-the-erasure-pathway-e1d350007509","t":"Soft-delete vs the right to erasure","d":"Article no. 36","k":"Proof & getting started","x":"Soft-delete for lifecycle, anonymization plus outbox purge for GDPR/CCPA erasure, and why both exist.","e":1},{"u":"https://medium.com/@ivanball76/the-test-pyramid-not-the-ice-cream-cone-1-880-fast-tests-zero-docker-cb459fda73d9","t":"The test pyramid","d":"Article no. 35","k":"Proof & getting started","x":"How the framework's tests stack up: fast unit and architecture tests at the base, E2E at the tip.","e":1},{"u":"https://medium.com/@ivanball76/architecture-fitness-functions-rules-that-fail-the-build-not-a-wiki-page-6562940deceb","t":"Architecture fitness functions","d":"Article no. 34","k":"Proof & getting started","x":"Architecture rules that fail the build: a compile-time layer guard plus a shared NetArchTest rule library.","e":1},{"u":"https://medium.com/@ivanball76/retries-are-not-a-recovery-plan-resilience-handlers-rto-rpo-and-a-restore-you-actually-drilled-3c7474814123","t":"Resilience and recovery objectives","d":"Article no. 33","k":"Run & extract","x":"Standard resilience on every outbound client, plus declared RTO/RPO and a drilled restore.","e":1},{"u":"https://medium.com/@ivanball76/extracting-a-module-to-a-grpc-service-live-799926cf8a32","t":"Extracting a module to a gRPC service","d":"Article no. 32","k":"Run & extract","x":"A step-by-step extraction of an in-process module into its own gRPC service, database, and auth.","e":1},{"u":"https://medium.com/@ivanball76/aspire-one-command-brings-up-the-whole-distributed-app-379b5cffdeed","t":"Aspire: one command","d":"Article no. 31","k":"Run & extract","x":"Model services, databases, and the broker as one Aspire graph that runs from laptop to Azure with one command.","e":1},{"u":"https://medium.com/@ivanball76/defending-the-api-edge-three-controls-that-cover-the-whole-surface-d958ecae1091","t":"Rate limiting and brute-force protection","d":"Article no. 30","k":"Auth & the edge","x":"Two layers that cover the whole API edge: endpoint rate limits plus lockout-based brute-force defense on identity.","e":1},{"u":"https://medium.com/@ivanball76/resource-ownership-authorization-which-rows-you-may-touch-not-just-which-actions-cb8e78867bae","t":"Resource-ownership authorization","d":"Article no. 29","k":"Auth & the edge","x":"Beyond roles and permissions: which rows you may touch, enforced per resource.","e":1},{"u":"https://medium.com/@ivanball76/generic-entity-controllers-and-the-dynamic-query-contract-adr-034-2b5c799bc69f","t":"Generic entity controllers","d":"Article no. 28","k":"Auth & the edge","x":"A write-once REST surface every entity inherits, plus a bounded dynamic query contract that is never open SQL.","e":1},{"u":"https://medium.com/@ivanball76/one-rotating-refresh-token-and-reuse-detection-that-makes-theft-self-limiting-fab42234a04a","t":"One rotating refresh token","d":"Article no. 27","k":"Auth & the edge","x":"A short-lived JWT plus one server-stored refresh token that rotates on every use, with reuse detection that makes a stolen token end its own session.","e":1},{"u":"https://medium.com/@ivanball76/google-and-github-login-without-leaking-tokens-external-oauth-behind-your-own-jwts-d68ba5e3aca4","t":"External OAuth login behind your own JWTs","d":"Article no. 26","k":"Auth & the edge","x":"Sign in with Google or GitHub without leaking provider tokens: external identity exchanged for your own JWTs at the boundary.","e":1},{"u":"https://medium.com/@ivanball76/browser-session-cookie-auth-for-blazor-ssr-surviving-the-f5-eb0ea317820e","t":"Browser session-cookie auth for Blazor SSR","d":"Article no. 25","k":"Auth & the edge","x":"HttpOnly session cookies and an SSR-time scheme so [Authorize] passes during prerender, with the API still the boundary.","e":1},{"u":"https://medium.com/@ivanball76/permission-based-authorization-capabilities-over-role-checks-ea6574cbee27","t":"Permission-based authorization over roles","d":"Article no. 24","k":"Auth & the edge","x":"A capability layer over RBAC: permission policies that resolve on demand from a central registry.","e":1},{"u":"https://medium.com/@ivanball76/delete-automapper-explicit-compile-time-dto-mapping-that-you-can-actually-test-9c7013cc5d3f","t":"Delete AutoMapper: manual DTO mapping","d":"Article no. 23","k":"Auth & the edge","x":"Why source-generated, per-entity mappers beat reflection-based mapping for clarity and speed.","e":1},{"u":"https://medium.com/@ivanball76/ephemeral-by-design-sub-second-live-channels-over-one-signalr-hub-0248050e0c8b","t":"Live channels over one SignalR hub","d":"Article no. 22","k":"Auth & the edge","x":"Sub-second ephemeral events (polls, Q&A, live counts) fanned out over the existing notification hub, with nothing persisted.","e":1},{"u":"https://medium.com/@ivanball76/notifications-as-a-vertical-slice-in-app-inbox-real-time-push-native-push-and-email-c59d5a4f3b69","t":"Notifications as a vertical slice","d":"Article no. 21","k":"Auth & the edge","x":"A notifications feature built as a clean vertical slice across every layer.","e":1},{"u":"https://medium.com/@ivanball76/problem-details-across-http-and-grpc-rfc-9457-9f20157cf7de","t":"Problem Details across HTTP and gRPC","d":"Article no. 20","k":"Auth & the edge","x":"One error contract mapped consistently to HTTP Problem Details and gRPC status.","e":1},{"u":"https://medium.com/@ivanball76/the-self-invalidating-cache-that-lives-in-the-pipeline-not-your-handlers-e11548062d2f","t":"The self-invalidating cache","d":"Article no. 19","k":"Auth & the edge","x":"A caching decorator where commands invalidate and queries populate, plus an authenticated output-cache tier at the API edge.","e":1},{"u":"https://medium.com/@ivanball76/idempotency-in-one-attribute-safe-retries-for-http-apis-065848fd03f4","t":"Idempotency in one attribute","d":"Article no. 18","k":"Auth & the edge","x":"Dedup client retries with an Idempotency-Key header and cached replay, plus a consumer-side inbox for brokers.","e":1},{"u":"https://medium.com/@ivanball76/password-hashing-done-right-pbkdf2-sha512-600k-iterations-timing-safe-d64ddb802403","t":"Password hashing done right","d":"Article no. 17","k":"Auth & the edge","x":"The non-negotiables of password storage in .NET, done correctly and tested.","e":1},{"u":"https://medium.com/@ivanball76/cross-service-auth-without-a-shared-secret-jwks-dual-fetch-478e6f688c7e","t":"JWKS cross-service auth","d":"Article no. 16","k":"Auth & the edge","x":"Validate another service's RS256 tokens via JWKS discovery, with no shared secret crossing a boundary.","e":1},{"u":"https://medium.com/@ivanball76/event-schema-versioning-never-silently-reshape-an-event-93cd5d4a156d","t":"Event-schema versioning","d":"Article no. 15","k":"Data & persistence","x":"Every integration event carries a schema version; breaking changes get a new event type and an upcaster, never a silent reshape.","e":1},{"u":"https://medium.com/@ivanball76/self-ordering-modules-discovered-kahn-ordered-and-extractable-2ce7283a26b5","t":"Self-ordering modules","d":"Article no. 14","k":"Data & persistence","x":"Modules declare their dependencies and load in topological order, so registration is never hand-sequenced.","e":1},{"u":"https://medium.com/@ivanball76/optimistic-concurrency-that-survives-the-round-trip-rowversion-from-database-to-dto-and-back-93d4a794716f","t":"Optimistic concurrency: RowVersion round-trips","d":"Article no. 13","k":"Data & persistence","x":"Carry the RowVersion from database to DTO and back, so a concurrent edit fails fast as a conflict instead of silently overwriting.","e":1},{"u":"https://medium.com/@ivanball76/ef-core-include-chains-are-a-trap-navigation-populators-decouple-eager-loading-c378fa4497ac","t":"Navigation populators","d":"Article no. 12","k":"Data & persistence","x":"Eager-load relationships that cross containers and data sources without N+1 or a leaky abstraction.","e":1},{"u":"https://medium.com/@ivanball76/one-entity-model-three-databases-polyglot-persistence-behind-a-single-attribute-760e77974d5d","t":"Polyglot persistence: one model, three engines","d":"Article no. 11","k":"Data & persistence","x":"SQL Server, Cosmos, and SQLite behind a single entity model, with the engine chosen by attribute.","e":1},{"u":"https://medium.com/@ivanball76/database-per-service-inside-a-monolith-and-why-265092eb03f1","t":"Database-per-service inside a monolith","d":"Article no. 10","k":"Data & persistence","x":"Give each module its own database and outbox before you extract it, so extraction changes hosting, not data.","e":1},{"u":"https://medium.com/@ivanball76/the-transactional-outbox-in-net-10-never-lose-an-event-again-f5a9b7a89e51","t":"The transactional outbox","d":"Article no. 9","k":"Core patterns","x":"Events that survive a crash: persist them atomically with your data, then dispatch at least once.","e":1},{"u":"https://medium.com/@ivanball76/compose-validators-dont-copy-them-a-reusable-fluentvalidation-kit-8865a6003a9c","t":"Compose validators, don't copy them","d":"Article no. 8","k":"Core patterns","x":"A validation kit that composes FluentValidation rules instead of copy-pasting them across features.","e":1},{"u":"https://medium.com/@ivanball76/the-cqrs-decorator-pipeline-logging-caching-and-transactions-without-touching-a-handler-fb7679b8bde8","t":"The CQRS decorator pipeline","d":"Article no. 7","k":"Core patterns","x":"Thin command and query handlers wrapped by a Scrutor decorator chain whose order is load-bearing.","e":1},{"u":"https://medium.com/@ivanball76/specifications-over-linq-spaghetti-composable-reusable-query-intent-8a40dafcbd3d","t":"Specifications over LINQ spaghetti","d":"Article no. 6","k":"Core patterns","x":"Compose queries from reusable specification objects instead of scattering LINQ across handlers.","e":1},{"u":"https://medium.com/@ivanball76/kill-the-anemic-domain-model-rich-aggregates-with-factory-methods-that-return-result-44f2e3d89794","t":"Kill the anemic domain model","d":"Article no. 5","k":"Core patterns","x":"Push behavior into rich aggregates with factory methods and invariants instead of bags of public setters.","e":1},{"u":"https://medium.com/@ivanball76/stop-throwing-exceptions-for-control-flow-the-result-railway-in-c-7a02050b554e","t":"The Result railway in C#","d":"Article no. 4","k":"Core patterns","x":"Model expected failures as Result values with a transport-agnostic error type, and keep exceptions for the genuinely exceptional.","e":1},{"u":"https://medium.com/@ivanball76/what-good-architecture-actually-means-a-34-category-rubric-you-can-score-yourself-against-4002291a6b6a","t":"The 34-category architecture rubric","d":"Article no. 3","k":"Orientation","x":"A two-axis rubric for scoring architecture on maturity and implementation, so 'good architecture' stops being a vibe.","e":1},{"u":"https://medium.com/@ivanball76/modular-monolith-to-microservices-without-the-rewrite-8c3603614f12","t":"Modular monolith to microservices","d":"Article no. 2","k":"Orientation","x":"The cornerstone idea: build the monolith now and extract a service later with no rewrite, via module discovery, gRPC contracts, and a YARP gateway.","e":1},{"u":"https://medium.com/@ivanball76/i-open-sourced-the-enterprise-net-77f9200f3728","t":"Open-sourced and graded against 34 categories","d":"Article no. 1","k":"Orientation","x":"Why I open-sourced a production .NET framework and scored it against a 34-category architecture rubric, gaps and all.","e":1}]} \ No newline at end of file diff --git a/docs-src/adr/001-manual-dto-mapping.md b/docs-src/adr/001-manual-dto-mapping.md index 335248c..431c41c 100644 --- a/docs-src/adr/001-manual-dto-mapping.md +++ b/docs-src/adr/001-manual-dto-mapping.md @@ -1,7 +1,7 @@ # ADR-001: Manual DTO Mapping over AutoMapper ## Status -Accepted. _Mechanism clarified 2026-06-26: the per-entity mappers are Riok.Mapperly source-generated (compile-time), not hand-written line by line. The decision to avoid runtime convention/reflection mapping (AutoMapper, Mapster) is unchanged._ _Mapper count refreshed 2026-08-14: 30 DTO mappers across Store + ADC._ +Accepted. _Mechanism clarified 2026-06-26: the per-entity mappers are Riok.Mapperly source-generated (compile-time), not hand-written line by line. The decision to avoid runtime convention/reflection mapping (AutoMapper, Mapster) is unchanged._ _Mapper count refreshed 2026-08-23: 31 DTO mappers across Store + ADC._ ## Context Domain entities must be mapped to DTOs for API responses. The two common approaches are: @@ -19,5 +19,5 @@ Use explicit, per-entity DTO mappers (each a Riok.Mapperly `[Mapper] partial cla - **Performance**: No reflection or expression compilation at mapping time. ## Trade-offs -- More files (30 DTO mappers across Store + ADC: 19 in ADC, 11 in Store, plus the parallel `IEntityRequestMapper` classes). The interface's default `MapToDTOs` implementation is available to remove the batch-mapping boilerplate, though in practice each concrete mapper re-declares the identical one-line projection rather than relying on the default. +- More files (31 DTO mappers across Store + ADC: 20 in ADC, 11 in Store, plus the parallel `IEntityRequestMapper` classes). The interface's default `MapToDTOs` implementation is available to remove the batch-mapping boilerplate, though in practice each concrete mapper re-declares the identical one-line projection rather than relying on the default. - Adding a new entity requires creating a mapper class. This is consistent with the project's explicit-over-implicit philosophy. diff --git a/docs-src/adr/007-grpc-extraction.md b/docs-src/adr/007-grpc-extraction.md index 713caff..f918cb5 100644 --- a/docs-src/adr/007-grpc-extraction.md +++ b/docs-src/adr/007-grpc-extraction.md @@ -1,7 +1,9 @@ # ADR-007: Synchronous Cross-Service Calls via gRPC Contracts ## Status -Accepted. +Accepted. Revised 2026-08-23 (the `[ServiceContract]` marker now has a dedicated fitness rule behind +it, `ServiceContractPurityTestsBase`, subclassed in all four repos; it is a ratchet that no marked +type triggers yet). ## Context Once modules became separate service processes, the in-process interface calls between them (e.g. @@ -41,15 +43,22 @@ Use **gRPC**, exposed through `MMCA.Common.Grpc`, with a contract-package conven Application/Domain/Shared, so the choice is reversible and the core stays clean. - **Strong contracts**: the `.proto` definitions plus the in-process interface the adapter implements are the wire surface. A `[ServiceContract(version)]` attribute (MMCA.Common.Shared) is - provided to mark and version contract types explicitly, but it is an **available extension point, not yet - applied**: no contract type carries it today, and no fitness rule enforces it; the wire surface is - currently defined by the `.proto` files alone. + provided to mark and version contract types explicitly, and a fitness rule stands behind it: + `ServiceContractPurityTestsBase.ServiceContracts_ShouldNotDependOn_ServiceInternals` + (MMCA.Common.Testing.Architecture, rule body + `ArchitectureRules.ServiceContractsDoNotDependOnServiceInternals`) scans every assembly the repo's + architecture map registers and fails any marked type that depends on the producing service's + Domain, Application or Infrastructure. All four repos subclass it. The rule is a **ratchet, not yet + triggered**: no contract type carries the attribute today, so the test passes without asserting + anything, and the wire surface is currently defined by the `.proto` files alone. It bites in a repo + the moment its first contract type is marked. ## Trade-offs - **Bidirectional pairs need care.** Conference ↔ Engagement is a mutual gRPC pair; the AppHost deliberately omits a reciprocal `WaitFor` to avoid a startup deadlock: transient "peer not ready" errors self-heal via the resilience pipeline. - **h2c assumptions.** Target services must serve HTTP/2 on cleartext; the Notification service runs - `Http1AndHttp2` for its SignalR WebSocket upgrade, unlike the `Http2`-only REST services. + `Http1AndHttp2` on its default endpoint for its SignalR WebSocket upgrade, unlike the `Http2`-only + REST services, and carries a second, `Http2`-only `grpc` endpoint for its gRPC ingress (ADR-012). - **Operational surface.** gRPC adds proto tooling, service discovery, and resilience tuning to the deployment. diff --git a/docs-src/adr/012-grpc-host-transport.md b/docs-src/adr/012-grpc-host-transport.md index 79a99c5..9e8f528 100644 --- a/docs-src/adr/012-grpc-host-transport.md +++ b/docs-src/adr/012-grpc-host-transport.md @@ -89,16 +89,16 @@ adds a dedicated `Http1`-only Kestrel listener whose only job is to answer the p `httpGet` probes, on a port that is never exposed via ingress: - The three Profile A hosts (Identity, Conference, Engagement) listen on **8081**. Bicep injects - `HealthProbe__Port=8081` (`MMCA.ADC/infra/main.bicep:1076`, `:1267`, `:1387`) and points startup, - liveness, and readiness at it (three `httpGet` probes on 8081, `main.bicep:1183-1207` for - Identity, under the explanatory comment at `:1176-1182`). The listener is added from each + `HealthProbe__Port=8081` (`MMCA.ADC/infra/main.bicep:1088`, `:1285`, `:1410`) and points startup, + liveness, and readiness at it (three `httpGet` probes on 8081, `main.bicep:1200-1225` for + Identity, under the explanatory comment at `:1193-1199`). The listener is added from each service's `Program.cs` (`MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:81`, Conference `Program.cs:85`, Engagement `Program.cs:68`); the per-service `KestrelConfiguration.ConfigureHttp2WithHealthProbe` helper this update originally named no longer exists and has been replaced by a shared framework method (see the 2026-08-07 update below). - The mixed-endpoint host (Notification) listens on **8082**, one port above its `grpc` endpoint: - `HealthProbe__Port=8082` at `main.bicep:1530`, with all three probes on 8082 at - `main.bicep:1583-1607`. Its listener is added from + `HealthProbe__Port=8082` at `main.bicep:1558`, with all three probes on 8082 at + `main.bicep:1615-1640`. Its listener is added from `MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:71` and is strictly additive on top of the config-declared 8080 and 8081 endpoints, explicitly so that all four services probe the same way. @@ -116,10 +116,10 @@ DB-aware check), which is why ADC moved, and (two days later) why Store followed The two-argument call sites are all in the AppHost (`MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:271-273`), which configures the local Aspire environment only. In production ACA, ADC's Bicep hardcodes the **direct in-cluster authority** -`http://${identityApp.name}` on every token-validating service: Conference (`main.bicep:1284`), -Engagement (`:1403`), and Notification (`:1546`). Identity's own ingress is `transport: 'http2'` -(`main.bicep:1037`; Conference `:1238` and Engagement `:1362` match, while Notification `:1485`, -the Gateway `:1645`, and the UI `:1747` stay `'http'`), so envoy accepts the HTTP/1.1 JwtBearer +`http://${identityApp.name}` on every token-validating service: Conference (`main.bicep:1302`), +Engagement (`:1426`), and Notification (`:1574`). Identity's own ingress is `transport: 'http2'` +(`main.bicep:1048`; Conference `:1255` and Engagement `:1384` match, while Notification `:1512`, +the Gateway `:1677`, and the UI `:1780` stay `'http'`), so envoy accepts the HTTP/1.1 JwtBearer metadata fetch and carries it to the container. That is exactly the arrangement the Store update above describes, so the "JWKS authority differs by environment" nuance is **not** Store-specific: both apps route discovery @@ -143,14 +143,14 @@ HTTP/1.1 `httpGet`.** The pattern is now uniform: `MMCA.Store/Source/Services/MMCA.Store.Identity.Service/Program.cs:65` and `MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:58`. - **Bicep injects the port and points startup, liveness, and readiness at it.** Store sets - `HealthProbe__Port=8081` on Identity (`MMCA.Store/infra/main.bicep:1064`) and Catalog (`:1164`), + `HealthProbe__Port=8081` on Identity (`MMCA.Store/infra/main.bicep:1082`) and Catalog (`:1188`), with `/alive` for startup and liveness and `/health/ready` for readiness on 8081 - (`main.bicep:1077-1079` and `:1176-1178`). ADC is unchanged (8081 for the three Profile A hosts, + (`main.bicep:1095-1097` and `:1200-1202`). ADC is unchanged (8081 for the three Profile A hosts, 8082 for Notification): its Bicep line anchors drift with unrelated observability commits, and - were last re-verified and corrected in the 2026-07-25 section above on 2026-08-14. + were last re-verified and corrected in the 2026-07-25 section above on 2026-08-23. - **Hosts whose default endpoint never went Http2-only keep probing their traffic port.** Store's - Sales, Gateway, and UI probe 8080 directly (`MMCA.Store/infra/main.bicep:1303-1305`, `:1376-1378`, - `:1473-1475`), because an `Http1AndHttp2` endpoint answers the HTTP/1.1 probe on its own. + Sales, Gateway, and UI probe 8080 directly (`MMCA.Store/infra/main.bicep:1333-1335`, `:1407-1409`, + `:1505-1507`), because an `Http1AndHttp2` endpoint answers the HTTP/1.1 probe on its own. Rule: the dedicated probe listener is part of Profile A, not an app-specific workaround. A host that goes `Http2`-only on cleartext gets the extra `Http1` listener plus `HealthProbe__Port` in its Bicep @@ -196,14 +196,14 @@ mixed-endpoint profile on the shared helper, `Program.cs:76`. See the 2026-08-14 **2. Notification's `grpc` endpoint now serves two gRPC services, one of them authorized.** The mixed-endpoint profile is no longer a one-edge exception. Notification maps `LiveChannelGrpcService` (the best-effort Engagement push ingress, -`MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:280`) and -`UserNotificationExportGrpcService` with `.RequireAuthorization()` (`:288`) on the same `Http2`-only +`MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:286`) and +`UserNotificationExportGrpcService` with `.RequireAuthorization()` (`:294`) on the same `Http2`-only `grpc` endpoint. The second consumer is Identity: its data-subject export aggregates the user's notification inbox rows through the client registered at `MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:281`, wired by `identityService.WithReference(notificationService)` locally (`MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:237`) and by the same -`services__notification__grpc__0` discovery variable in production (`MMCA.ADC/infra/main.bicep:1120`, +`services__notification__grpc__0` discovery variable in production (`MMCA.ADC/infra/main.bicep:1132`, pointing at the `additionalPortMappings` h2c port). The contracts are `Protos/live_channel.proto:19` and `Protos/user_notification_export.proto:17` in `MMCA.ADC.Notification.Contracts`. Transport-wise nothing changes, which is the point: the one extra @@ -240,7 +240,7 @@ REST routes plus the Stripe webhook over HTTP/1.1 `builder.ConfigureEndpointsWithHealthProbe(HttpProtocols.Http1AndHttp2, redeclareCleartextEndpoint: false)` (`Program.cs:76`), exactly as Notification does. - **The gRPC service is authorized, not merely internal:** `UserSalesExportGrpcService` is mapped - with `.RequireAuthorization()` (`Program.cs:291`) because it returns personal data keyed by a raw + with `.RequireAuthorization()` (`Program.cs:293`) because it returns personal data keyed by a raw `CustomerId`; the caller forwards the end user's JWT. That matches the reasoning recorded for ADC's export endpoint in the 2026-08-07 update. - **Discovery:** Identity registers the client with `AddSalesUserExportClient()` @@ -250,12 +250,12 @@ REST routes plus the Stripe webhook over HTTP/1.1 AppHost wires `identityService.WithReference(salesService)` (`MMCA.Store/Source/Hosting/MMCA.Store.AppHost/Program.cs:199`); in production the Bicep injects `services__sales__grpc__0 = http://-sales:8081` on Identity - (`MMCA.Store/infra/main.bicep:1040`). -- **ACA:** Sales' main ingress stays `transport: 'http'` (`main.bicep:1209`) and the gRPC port is an - internal TCP `additionalPortMappings` entry on 8081 (`:1216-1222`), the same TCP-passthrough + (`MMCA.Store/infra/main.bicep:1052`). +- **ACA:** Sales' main ingress stays `transport: 'http'` (`main.bicep:1233`) and the gRPC port is an + internal TCP `additionalPortMappings` entry on 8081 (`:1240-1246`), the same TCP-passthrough arrangement ADC's Notification uses. Sales still needs no `HealthProbe__Port`: its `Http1AndHttp2` default endpoint answers the platform's HTTP/1.1 probes on 8080 directly - (`:1303-1305`). + (`:1333-1335`). Consequence for this ADR's taxonomy: **no deployed host is pure Profile B any more.** `Http1AndHttp2` survives in both apps only as the default-endpoint half of the mixed-endpoint @@ -316,7 +316,7 @@ Use when services must **serve** gRPC on cleartext (any bidirectional / inbound inbound gRPC from a dedicated `Http2`-only named endpoint (the mixed-endpoint profile in the update above), today two services on that one endpoint: `LiveChannelGrpcService` and the authorized `UserNotificationExportGrpcService` - (`MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:280`, `:288`). So "serves no + (`MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:286`, `:294`). So "serves no inbound gRPC" no longer holds for the host, only for its default endpoint. ### Profile B: `Http1AndHttp2` + HTTPS/ALPN + `ForwardHttp2=false` + direct JWKS (Store's original choice; now retained only as the default-endpoint half of the mixed-endpoint profile) diff --git a/docs-src/adr/014-cqrs-decorator-pipeline.md b/docs-src/adr/014-cqrs-decorator-pipeline.md index ab6ea5c..118c1de 100644 --- a/docs-src/adr/014-cqrs-decorator-pipeline.md +++ b/docs-src/adr/014-cqrs-decorator-pipeline.md @@ -85,10 +85,10 @@ The pipeline order and the "cache invalidation outside the transaction" rule are ## Revision (2026-08-18) **Two decorators were added to both chains, so the order recorded in the Decision above is no longer the shipped one.** The registration site is unchanged in kind: `AddApplicationDecorators()` -(`MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:102`) still uses Scrutor -`TryDecorate` and still documents the reverse-registration rule inline (`:49-51`), now with ASCII -nesting diagrams of both chains beside it (`:53-74`). The literal registration sequence is -`:107-113` for commands and `:116-120` for queries, so the execution order (outermost to innermost) is +(`MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:110`) still uses Scrutor +`TryDecorate` and still documents the reverse-registration rule inline (`:56-59`), now with ASCII +nesting diagrams of both chains beside it (`:60-83`). The literal registration sequence is +`:115-121` for commands and `:124-128` for queries, so the execution order (outermost to innermost) is now: - **Commands:** FeatureGate -> Authorization -> Logging -> Caching -> Validating -> Timeout -> @@ -137,10 +137,10 @@ now: **Two placements are load-bearing and are argued in code, not only here.** Authorization sits **outside** caching deliberately: a cache lookup ahead of the permission check would serve another caller's rows to a principal not allowed to run the query, so a denied request must neither read nor -populate the cache (`DependencyInjection.cs:83-85`, restated at +populate the cache (`DependencyInjection.cs:91-93`, restated at `AuthorizationCommandDecorator.cs:13-16`, which also notes that a denied command never starts a transaction and never runs validation). FeatureGate stays outside Authorization so that a disabled -feature does not leak which permission guards it (`DependencyInjection.cs:79-82`), which preserves +feature does not leak which permission guards it (`DependencyInjection.cs:87-90`), which preserves ADR-031's "disabled is indistinguishable from nonexistent" property. Timeout sits **inside** validation and **outside** the transaction, so an invalid command never consumes budget and an expired budget still unwinds through the transactional decorator's rollback path. @@ -153,9 +153,15 @@ the innermost element is not itself a decorator (`:95-96`). Both expected sequen `protected virtual`, so a consumer with a different chain can override them. MMCA.Common subclasses it against its own registration sequence (`MMCA.Common/Tests/Hosting/MMCA.Common.Testing.Tests/DecoratorPipelineOrderTests.cs:21-39`) without -overriding either list, so the base order is pinned in Common's default test pass. Whether ADC, Store -or Helpdesk subclass it was not verified for this revision; treat cross-repo coverage as unconfirmed. -This closes the "one place to read the pipeline" claim in the Rationale, which until now rested +overriding either list, so the base order is pinned in Common's default test pass. ADC, Store and +Helpdesk each subclass the same base +(`MMCA.ADC/Tests/Architecture/MMCA.ADC.Architecture.Tests/DecoratorPipelineOrderTests.cs:28`, +`MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/DecoratorPipelineOrderTests.cs:27`, +`MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:31`), +and none of the three overrides either list: the only definitions of `ExpectedCommandDecorators` and +`ExpectedQueryDecorators` workspace-wide are the base's own +(`DecoratorPipelineOrderTestsBase.cs:49,61`). All four repos therefore pin the same chain against +their own genuine registration sequence. This closes the "one place to read the pipeline" claim in the Rationale, which until now rested entirely on the inline comments the Trade-offs cite as the mitigation for the Scrutor foot-gun. The trade-off list above gains one entry by construction: the chain is now seven decorators deep for a diff --git a/docs-src/adr/015-architecture-fitness-functions.md b/docs-src/adr/015-architecture-fitness-functions.md index 2f596fd..3f46a3a 100644 --- a/docs-src/adr/015-architecture-fitness-functions.md +++ b/docs-src/adr/015-architecture-fitness-functions.md @@ -8,8 +8,11 @@ Revision (2026-08-18) at the end). Revised again the same day for the Section B families join the library, a `.proto` wire-contract gate and an idempotency-intent gate, and the counts move to **104 test methods across 36 bases**, superseding the 102/34 figure the first revision recorded. See the second section, Revision (2026-08-18): Section B rule families. Revised 2026-08-18 -against the released framework v1.154.0: MMCA.Common's own build now executes **99** of the 104 methods -(`FACTS.md:47`) and the public-API baselines hold **5,150 declarations**. +against the released framework v1.154.0: MMCA.Common's own build then executed **99** of the 104 methods +and the public-API baselines hold **5,150 declarations**. Revised 2026-08-23: the method and base counts +moved again and are no longer restated in this record's live text, since `MMCA.Common/FACTS.md` owns them +(`:44`, `:47`) and is drift-gated in CI; the Decision's `--minimum-expected-tests` figure is corrected to +the floor CI actually applies. See Revision (2026-08-23) at the end. ## Context The codebase rests on invariants that are easy to state and easy to erode by accident: clean- @@ -40,8 +43,9 @@ Enforce architectural invariants as **automated checks that gate the build**, in identically everywhere via NetArchTest over the compiled assemblies. These tests run inside the normal `dotnet test` / CI tier, so a violated invariant fails CI like any -other test (CI additionally guards with `--minimum-expected-tests 1` so an empty suite cannot pass -silently). Centralizing the rules in a package, rather than copying them per repo, means a new +other test (the whole-solution run that carries them is additionally floored at +`--minimum-expected-tests 2000`, `.github/workflows/ci.yml:144`, so a discovery or filter regression +that silently drops the suite fails the job instead of passing green on a handful of tests). Centralizing the rules in a package, rather than copying them per repo, means a new invariant is written once and inherited by every consumer. ## Rationale @@ -176,12 +180,14 @@ instead of something a consumer discovers after the release. The baselines hold **5,150 declarations** across the fourteen files (5,164 non-empty lines, each file opening with a `#nullable enable` header), and every `PublicAPI.Unshipped.txt` contains that header and -nothing else. **What is baselined is the surface as of this branch**, which is now the released -framework v1.154.0 (`MMCA.Common/FACTS.md:14`), so the figures above cover the Section A additions this +nothing else. **What is baselined is the surface as of this branch**, which is whichever release +`MMCA.Common/FACTS.md:14` currently names (v1.154.0 when this revision was written, v1.160.0 as of +2026-08-23), so the figures above cover the Section A additions this revision described plus the Section B ones that followed, not a frozen picture of one release. The start version is no longer uncited: the gate shipped in v1.153.0, whose changelog entry names `Microsoft.CodeAnalysis.PublicApiAnalyzers` on every in-slnx Source project with committed baselines as -one of three new build gates (`MMCA.Common/CHANGELOG.md:118-124`). The discipline therefore begins with +one of three new build gates (`MMCA.Common/CHANGELOG.md:297-303`, under the v1.153.0 heading at +`:242`). The discipline therefore begins with v1.153.0 rather than applying retroactively. Three rules from the same analyzer are off, each with the reason recorded rather than silently @@ -230,9 +236,10 @@ with a different wave and changes a number that revision states. Two rule famili library, and both are **consumer-facing**: the framework ships the rule and exercises it, but the thing being pinned lives in a consumer repository. -**The counts in the preceding revision are superseded.** `MMCA.Common/FACTS.md:44` now reads 104 test -methods across 36 abstract `*TestsBase` classes, of which MMCA.Common's own build executes 99 -(`FACTS.md:47`), where the entry above recorded 102 across 34 with 87 executed. Those figures were +**The counts in the preceding revision are superseded.** `MMCA.Common/FACTS.md:44` read, on the date of +this entry, 104 test methods across 36 abstract `*TestsBase` classes, of which MMCA.Common's own build +executed 99 (`FACTS.md:47`), where the entry above recorded 102 across 34 with 87 executed (both are +themselves superseded: see Revision (2026-08-23)). Those figures were correct when written and are left in place, consistent with how this library treats a superseded count. Cite `FACTS.md` rather than either number: it is generated by `FactsGenerator` and gated against drift in CI, so it cannot quietly disagree with the code the way a transcribed figure can. @@ -291,6 +298,45 @@ belong, in [ADR-017](017-request-idempotency.md)'s Revision (2026-08-18). hole, which is what keeps them adoptable and also what makes "the suite is green" a weaker statement than it sounds. +## Revision (2026-08-23): superseded counts, a re-anchored citation, and the real test floor +No rule family joined or left the library in this entry. It corrects three things the two 2026-08-18 +revisions above state, and it is kept as its own entry rather than edited into them because those +entries were correct when written. + +**The counts are superseded a second time, and this record stops restating them.** +`MMCA.Common/FACTS.md` is generated by `FactsGenerator` and gated against drift in CI, so it is the +only place either figure can be read without a transcription risk. As of 2026-08-23 (framework +v1.160.0, `FACTS.md:4`, `:14`) it reads **110 test methods across 38 abstract `*TestsBase` classes** +(`FACTS.md:44`), of which MMCA.Common's own build executes **129** (`FACTS.md:47`), where the first +revision recorded 102 across 34 with 87 executed and the Section B entry recorded 104 across 36 with 99 +executed. Read `FACTS.md`, not this paragraph, for the current values. + +**The "N of M executed" framing in the Section B cost list no longer parses, and that is the honest +correction, not a rounding.** The executed figure now exceeds the shipped method count because it +counts the methods of the bases MMCA.Common subclasses **plus** its Common-only direct tests +(`FrameworkSanityTests`, `SpecificationFitnessTests`), which `FACTS.md:47` says in its own parenthetical. +So "the framework executes 99 of 104" cannot be restated as "129 of 110": the two numbers are no longer +a subset and its container. The gap that bullet was pointing at, consumer-facing families whose subject +lives in another repository, is unchanged and still real (the proto rule runs against fixtures, the +idempotency rule needs a consumer's controllers); it simply cannot be measured by subtracting one +`FACTS.md` line from the other any more. + +**The Decision's test-count floor was wrong and is corrected in place.** The architecture tests have no +floor of their own: they run inside the whole-solution step of `build-and-test`, which is floored at +`--minimum-expected-tests 2000` against a suite of roughly 2,254 (`.github/workflows/ci.yml:141-144`). +The value 1 belongs to a different job entirely, the `ui-e2e` matrix, which runs the out-of-slnx +`MMCA.Common.UI.E2E.Tests` project by path (`ci.yml:301`). The correction makes the guard weaker than +the Decision claimed in one specific way worth naming: a floor of 2,000 over a 2,254-test suite would +not notice the architecture suite disappearing on its own, only a collapse of the whole run. A +per-project floor is only available to a job that runs one project by path, which is what the `ui-e2e` +job does and the solution-wide run cannot. + +**One citation was re-anchored, not rewritten.** The v1.153.0 changelog bullet naming +`Microsoft.CodeAnalysis.PublicApiAnalyzers` is verbatim unchanged but has moved to +`MMCA.Common/CHANGELOG.md:297-303` (its release heading at `:242`) as newer releases were prepended +above it. Line-anchored citations into an append-at-top file are a known cost of citing this precisely; +the alternative, citing nothing, is worse. + ## Related ADR-009 (resilience gate), ADR-010 (event-version gate), ADR-016 (MassTransit pin gate, and the lockstep release cadence the public API baseline is pinned to), ADR-006/007/008 (the transport and diff --git a/docs-src/adr/020-permission-based-authorization.md b/docs-src/adr/020-permission-based-authorization.md index 56d253e..6d4b3a4 100644 --- a/docs-src/adr/020-permission-based-authorization.md +++ b/docs-src/adr/020-permission-based-authorization.md @@ -1,7 +1,7 @@ # ADR-020: Permission-Based Authorization Layered over Roles ## Status -Accepted (2026-06-25, amended 2026-07-10). +Accepted (2026-06-25, amended 2026-07-10 and 2026-08-23). ## Context Authorization started as pure role-based access control (RBAC). Endpoints declared the role they @@ -49,9 +49,10 @@ Add a **permission (capability) layer over RBAC**, opt-in and backward-compatibl free, but it grants nothing beyond explicit claims until a host calls `AddPermissions(...)`. The existing role policies keep working unchanged. -Adoption is asymmetric and that is intentional: ADC's Conference module defines eight capabilities -(`ConferencePermissions.cs:12-33`, enumerated in `All` at `:36-46`), including a curation subset -granted to a new `RoleNames.ContentEditor`; its Engagement module defines three +Adoption is asymmetric and that is intentional: ADC's Conference module defines nine capabilities +(`ConferencePermissions.cs:12-36`, enumerated in `All` at `:39-50`), including a curation subset +(`ContentManagement` at `:57-64`: sessions, speakers, categories, sponsors, activities) granted to a +new `RoleNames.ContentEditor`; its Engagement module defines three (`engagement:live:manage` gating the conference-day live-poll management endpoints, `engagement:checkin:manage` gating QR badge check-in and the attendance rollup, and `engagement:points:view-overview` gating the organizer points rollup, at diff --git a/docs-src/adr/021-consumer-inbox-idempotency.md b/docs-src/adr/021-consumer-inbox-idempotency.md index 58adffc..c398139 100644 --- a/docs-src/adr/021-consumer-inbox-idempotency.md +++ b/docs-src/adr/021-consumer-inbox-idempotency.md @@ -64,13 +64,13 @@ because that per-service project postdates the frozen combined-archive lineage t migration. Adoption inventory as of 2026-08-13: **three ADC services consume from the broker** and so use their inbox for real, plus Store Sales. ADC Identity consumes `SpeakerLinkedToUser` and `SpeakerUnlinkedFromUser` (`MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:299-300`), -ADC Conference consumes `UserRegistered` (`MMCA.ADC.Conference.Service/Program.cs:365`), and ADC +ADC Conference consumes `UserRegistered` (`MMCA.ADC.Conference.Service/Program.cs:372`), and ADC Engagement consumes four events, `AttendeeCheckedIn`, `SessionFeedbackSubmitted`, -`EventFeedbackSubmitted` and `UserDeleted` (`MMCA.ADC.Engagement.Service/Program.cs:299-302`), the +`EventFeedbackSubmitted` and `UserDeleted` (`MMCA.ADC.Engagement.Service/Program.cs:305-308`), the first of which is ADC's first **self-consumption** over the broker: Engagement publishes `AttendeeCheckedIn` and consumes it back, which is precisely the shape a redelivery would double-count, so the inbox is load-bearing there rather than decorative. Store Sales consumes `ProductVariantChanged` -(`MMCA.Store/Source/Services/MMCA.Store.Sales.Service/Program.cs:251`). Only **ADC Notification** +(`MMCA.Store/Source/Services/MMCA.Store.Sales.Service/Program.cs:253`). Only **ADC Notification** now carries `EnableInbox: true` and the table while registering no consumer, so its inbox alone is provisioned and unused (functionally harmless). The mirror image is equally harmless and worth stating because it looks like the opposite mistake: Store Catalog and Store Identity carry the `InboxMessages` @@ -123,12 +123,12 @@ What changed is that the default is now loud. 1. **A broker-connected host with no inbox says so at startup.** `AddBrokerMessaging` returns early for `MessageBusProvider.InProcess` - (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:656-659`), which is + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:669-672`), which is what scopes this to hosts that actually talk to a broker: in-process dispatch never redelivers, so there is nothing to warn about. Past that early return, the `EnableInbox` branch - (`:686-698`) registers `EfInboxStore` as scoped (`:688`) when the flag is set, and on the `else` - branch registers `NoOpInboxStore` as a singleton (`:692`) **plus** an `IHostedService` whose only - job is to emit one `Warning` line naming the consequence and the fix (`:697`). The service is + (`:699-711`) registers `EfInboxStore` as scoped (`:701`) when the flag is set, and on the `else` + branch registers `NoOpInboxStore` as a singleton (`:705`) **plus** an `IHostedService` whose only + job is to emit one `Warning` line naming the consequence and the fix (`:710`). The service is `InboxDisabledWarningService` (`.../Persistence/Inbox/InboxDisabledWarningService.cs:18-19`), and it evaluates nothing at runtime: `StartAsync` logs unconditionally (`:22-26`), because the condition was already decided by diff --git a/docs-src/adr/024-push-notifications.md b/docs-src/adr/024-push-notifications.md index cc0f9e6..13547ff 100644 --- a/docs-src/adr/024-push-notifications.md +++ b/docs-src/adr/024-push-notifications.md @@ -60,7 +60,7 @@ recipient policy both behind abstractions. (`SendPushNotificationHandler.cs:134-152`), an OS-level native-push channel that reaches devices the SignalR hub cannot (the app backgrounded or killed). It is best-effort by the same logic as the live push (a throw is logged, never fatal, and the SignalR leg has already decided the audit status), and it - defaults to `NullNativePushSender` (`MMCA.Common.Infrastructure`, `DependencyInjection.cs:478`), so it + defaults to `NullNativePushSender` (`MMCA.Common.Infrastructure`, `DependencyInjection.cs:491`), so it stays inert until a native hub is configured. The design of that channel is ADR-044's scope; this ADR keeps its own on the inbox and SignalR channels, so the "Two-Channel" title names the durable and transient channels this record governs, not a hard cap on the number of delivery legs. @@ -71,7 +71,7 @@ recipient policy both behind abstractions. `MapNotificationHub()` maps `NotificationHub` at the configured `HubPath` just when it is true (`SignalRExtensions.cs:25`). `AddPushNotifications` binds the section but registers SignalR, `SignalRPushNotificationSender` and `SignalRLiveChannelPublisher` unconditionally - (`DependencyInjection.cs:528-551`), so the opt-in registration, not the flag, is what decides whether + (`DependencyInjection.cs:541-564`), so the opt-in registration, not the flag, is what decides whether a send goes through SignalR; with `Enabled: false` the sender is still wired and simply has no hub endpoint for clients to connect to. @@ -124,10 +124,10 @@ unchanged: this closes a documentation gap so the asymmetry reads as deliberate single implementation, `SmtpEmailSender` (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Services/SmtpEmailSender.cs:12`), and it is TryAdd-registered in the same block as the push-sender defaults - (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:472`, beside - `IPushNotificationSender` at `:473`, `ILiveChannelPublisher` at `:474`, `INativePushSender` at `:478` - and `IPushDeviceRegistrar` at `:479`). That block is `AddServices()` (`:442`), which - `AddInfrastructure` (`:50`) always calls (`:154`), so every host gets it. Unlike the two push + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:485`, beside + `IPushNotificationSender` at `:486`, `ILiveChannelPublisher` at `:487`, `INativePushSender` at `:491` + and `IPushDeviceRegistrar` at `:492`). That block is `AddServices()` (`:455`), which + `AddInfrastructure` (`:50`) always calls (`:167`), so every host gets it. Unlike the two push abstractions it has no null default and no opt-in `Add*` counterpart: the real SMTP sender is always the registration, inert only because nothing resolves it. 2. **It sits outside the inbox / SignalR / native model.** An email creates no `PushNotification` audit diff --git a/docs-src/adr/026-caching-strategy.md b/docs-src/adr/026-caching-strategy.md index 1c84302..0fd1b17 100644 --- a/docs-src/adr/026-caching-strategy.md +++ b/docs-src/adr/026-caching-strategy.md @@ -8,7 +8,10 @@ path is unchanged; see the Revision (2026-08-13) below. Revised 2026-08-18: **Ti per-process**. An `OutputCacheEvictionRequested` integration event lets a mutation in one service evict another service's output cache over the existing outbox, broker and inbox path, per tag and best-effort, on a new `MMCA.Common.OutputCache` meter. Tier 1 is untouched by that change, which is the -mirror image of ADR-077's Tier-1-only amendment; see the Revision (2026-08-18) at the end. +mirror image of ADR-077's Tier-1-only amendment; see the Revision (2026-08-18) at the end. Revised +2026-08-23: the counter trade-off now records that `ICacheService`'s own `` still anticipates a +Redis `INCR` override that no implementation provides, so a reader who starts at the interface is not +left with the opposite conclusion; anchors re-verified throughout. ## Context The framework needs caching in two distinct places. Inside the application pipeline, query results @@ -36,7 +39,7 @@ Cache in two tiers, each with its own substrate. Caching decorators, `LoginProtectionService`) depends only on this interface, never on a concrete cache or on Redis. - **The backing store is chosen at startup, not in code (amended by ADR-077).** `AddCaching()` - (`MMCA.Common.Infrastructure/DependencyInjection.cs:164`, called from `AddInfrastructure`) registers + (`MMCA.Common.Infrastructure/DependencyInjection.cs:177`, called from `AddInfrastructure`) registers `DistributedCacheService` when a real `IDistributedCache` is present (one that is not the in-memory `MemoryDistributedCache`, i.e. Aspire registered Redis), and otherwise `MemoryCacheService`. The monolith with no distributed cache gets in-process caching for free; a host that wires Redis gets the @@ -65,8 +68,11 @@ Cache in two tiers, each with its own substrate. ### Tier 2: an HTTP output-cache edge - **The pipeline always enables it; policies are opt-in per host.** `MMCA.Common.API` calls - `app.UseOutputCache()` in the shared middleware pipeline - (`MMCA.Common.API/Startup/WebApplicationExtensions.cs:111`), but ships no policies. Each service + `app.UseOutputCache()` in the shared middleware pipeline, which is a builder of named steps rather + than a run of inline calls: the step is registered at + `MMCA.Common.API/Startup/MiddlewarePipelineBuilder.cs:138` under the name + `MiddlewarePipelineStepNames.OutputCache` (`MMCA.Common.API/Startup/MiddlewarePipelineStepNames.cs:64`). + The pipeline ships no policies. Each service registers its own `AddOutputCache(...)`: most declare a `NoCache` base policy (Identity, Sales, Engagement, Notification), while the read-heavy public services declare real cacheable policies. ADC Conference and Store Catalog are the adopters today, with named policies and `[OutputCache]` on their @@ -138,7 +144,7 @@ Cache in two tiers, each with its own substrate. hot objects cost more than the in-process path. - **Counter increments are not atomic, and that is the accepted position.** `ICacheService.IncrementAsync` is a default interface member implemented as a read-modify-write - (`MMCA.Common.Application/Interfaces/ICacheService.cs:57`), and `DistributedCacheService` overrides it + (`MMCA.Common.Application/Interfaces/ICacheService.cs:59`), and `DistributedCacheService` overrides it with the same read-modify-write shape rather than Redis `INCR` (`MMCA.Common.Infrastructure/Caching/DistributedCacheService.cs:127-133`). The reason is a storage format mismatch, documented at the implementation @@ -149,7 +155,12 @@ Cache in two tiers, each with its own substrate. login, in the ADR-029 case). A counter has to live in the same storage format as the reads that consult it, so readability wins over atomicity here: the ADR-029 brute-force and rate-limit counters can undercount under genuinely concurrent increments, and an occasional lost increment is the accepted - cost of a counter that is always readable. + cost of a counter that is always readable. One caveat for a reader who goes to the interface first: + its `` still anticipates the opposite outcome (backing stores that can do better with Redis + `INCR` override it, `MMCA.Common.Application/Interfaces/ICacheService.cs:54-57`). No implementation + does, and the one that could deliberately does not, for the storage-format reason above. Read that + comment as an option the framework declined, not as a description of a shipped override; the + implementation's own `` is the accurate one. - **Output caching is opt-in per service.** A read-heavy service that forgets to register a real `AddOutputCache` policy gets no edge caching (the `NoCache` base is the safe default), the same audit-the-inventory caveat as other opt-in capabilities (ADR-019/020/021). Adopting a policy is only @@ -165,7 +176,10 @@ substrate follows), ADR-040 (amends this ADR's Tier 2: the adopters' public-read authenticated, bearer-carrying requests too, not only anonymous traffic), [ADR-077](077-hybridcache-substrate.md) (amends this ADR's Tier 1: the opt-in `HybridCacheService` substrate, the disjoint `hc:` keyspace that generalizes the `WRONGTYPE` lesson recorded in the counter -trade-off above, and the L1 bypass that keeps `IncrementAsync` semantics unchanged). +trade-off above, and the L1 bypass that keeps `IncrementAsync` semantics unchanged), +[ADR-090](090-event-upcaster-registration.md) (the upcaster registration and the +`RegisterUpcastedIntegrationEventConsumer` sibling that a future reshape of this ADR's +`OutputCacheEvictionRequested` contract would go through). ## Revision (2026-07-24) Three substrate corrections from a code review. @@ -336,6 +350,10 @@ One substrate correction plus a line-anchor re-verification. No decision and no preceding revision treated its predecessor: the anchors it recorded were correct on 2026-08-07 and have since drifted again. +Items 3, 4 and 6 of this entry were correct on 2026-08-14 and have since drifted, and item 4's +`WebApplicationExtensions.cs` home no longer exists at all: read the entry as the state on that date, +and the 2026-08-23 entry below for the current anchors. + ## Revision (2026-08-18) Every previous revision moved Tier 1. This one moves **Tier 2**, and it is the first change to the output-cache edge since [ADR-040](040-authenticated-output-caching-for-public-reads.md). @@ -349,10 +367,10 @@ invalidation that crossed a boundary was the expiry clock. ### An integration event carries the eviction `OutputCacheEvictionRequested` -(`MMCA.Common/Source/Core/MMCA.Common.Domain/IntegrationEvents/OutputCacheEvictionRequested.cs:23`) is +(`MMCA.Common/Source/Core/MMCA.Common.Domain/IntegrationEvents/OutputCacheEvictionRequested.cs:27`) is a sealed record over `BaseIntegrationEvent` whose only own member is -`IReadOnlyList Tags { get; init; } = []` (`:31`), inheriting `SchemaVersion => 1` -(`.../DomainEvents/BaseIntegrationEvent.cs:22`) and therefore ADR-010's versioning contract. It is the +`IReadOnlyList Tags { get; init; } = []` (`:35`), inheriting `SchemaVersion => 1` +(`.../DomainEvents/BaseIntegrationEvent.cs:32`) and therefore ADR-010's versioning contract. It is the **first concrete integration event the framework itself ships**: a repository-wide search of `Source/` finds no other, every prior hit being the interface, the abstract base or generic plumbing. Until now the framework provided the delivery machinery and consumers provided all the messages. @@ -370,9 +388,13 @@ be: evicting twice is free, and evicting late is what a TTL is for. ### The consumer side is two registrations in two packages `RegisterOutputCacheEvictionConsumer(bool registerFaultConsumer = true)` -(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs:68-70`) +(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Services/IntegrationEventConsumerExtensions.cs:108-110`) is the MassTransit-side shorthand for `RegisterIntegrationEventConsumer()`, -so it inherits ADR-087's fault consumer by default. `AddOutputCacheEvictionHandler()` +so it inherits ADR-087's fault consumer by default. It now sits below a sibling in that same file, +`RegisterUpcastedIntegrationEventConsumer` (`:78-90`), the +[ADR-090](090-event-upcaster-registration.md) registration a host adds while a retired contract still +drains from a queue. The two are alternatives on one event, never both: registering a plain and an +upcasting consumer for the same type puts two consumers on one queue. `AddOutputCacheEvictionHandler()` (`MMCA.Common/Source/Presentation/MMCA.Common.API/Caching/OutputCacheEvictionExtensions.cs:32`) registers the handler itself, through `TryAddEnumerable` (`:36-38`). They are in different packages because the two halves are genuinely different concerns (broker registration is Infrastructure, output @@ -415,3 +437,46 @@ reason two "best effort" counters exist in one release. - **Tier 1 and Tier 2 remain separate invalidation models.** A single mutation may need a Tier 1 prefix invalidation and a Tier 2 tag eviction, and nothing coordinates them; ADR-077 moved one, this revision moves the other, and they still do not meet. + +## Revision (2026-08-23) +One correction of substance plus a line-anchor re-verification. No decision and no behavior changed. + +1. **The counter trade-off now names the contradiction a reader will hit.** `ICacheService`'s + `` on `IncrementAsync` says that backing stores which can do better with Redis `INCR` + override it (`MMCA.Common.Application/Interfaces/ICacheService.cs:54-57`). Nothing does. The only + store that could, `DistributedCacheService`, deliberately keeps the read-modify-write and documents + why at its own implementation (`DistributedCacheService.cs:107-133`). The Revision (2026-07-25) + already recorded that no `INCR` override exists or is intended, but this ADR never said that the + interface comment still points the other way, so a reader starting from the interface reached the + opposite conclusion. Trade-offs now says which of the two comments describes shipped behavior. The + source comment is unchanged by this entry; only the ADR is. +2. **Tier 2's `UseOutputCache` call has a new home, and a different shape of home.** + `app.UseOutputCache()` is no longer an inline call in + `MMCA.Common.API/Startup/WebApplicationExtensions.cs` (it is not in that file at all). The shared + middleware pipeline is now assembled from named steps: the output-cache step is at + `MMCA.Common.API/Startup/MiddlewarePipelineBuilder.cs:138`, named by + `MiddlewarePipelineStepNames.OutputCache` + (`MMCA.Common.API/Startup/MiddlewarePipelineStepNames.cs:64`). The decision is untouched, the + pipeline still always enables output caching and still ships no policies, but the citation now + points at a builder rather than at a sequence of calls. +3. **The eviction event's method surface gained an ADR-090 sibling.** + `RegisterOutputCacheEvictionConsumer` moved to + `IntegrationEventConsumerExtensions.cs:108-110` (from `:68-70`) because + `RegisterUpcastedIntegrationEventConsumer` (`:78-90`) was added above it, the registration + [ADR-090](090-event-upcaster-registration.md) defines for draining a retired contract. The + 2026-08-18 revision predates it and did not mention it; the consumer paragraph now does, including + that the two are alternatives on one event rather than a pair. ADR-090 is added to Related. +4. **Remaining anchors.** `AddCaching` is now at + `MMCA.Common.Infrastructure/DependencyInjection.cs:177` (from `:164`). + `OutputCacheEvictionRequested`'s declaration is at `:27` and `Tags` at `:35` (from `:23` and `:31`), + both pushed down four lines by a Frozen-contract-candidate paragraph added to the doc comment. + `BaseIntegrationEvent.SchemaVersion` is at `:32` (from `:22`), likewise behind added doc text. + `ICacheService`'s `IncrementAsync` declaration is at `:59` (from `:57`; `:57` is now the last line of + the `` discussed in item 1). Re-checked and unchanged: `CacheOptions.cs:17` and `:22-25`, + both adopters' `AddStackExchangeRedisOutputCache` calls and all seven paired Redis registrations, + `DistributedCacheService.cs:108-126` and `:127-133`, the Tier 2 policy anchors + (`OutputCacheOptionsExtensions.cs:20`, `PublicEndpointOutputCachePolicy.cs:35`, `:71-75`, + `:109-113`), `OutputCacheEvictionExtensions.cs:32` and `:36-38`, `OutputCacheEvictionHandler.cs:32` + and `:44-63`, `OutputCacheMetrics.cs:19` and `:29-37`, and `MMCA.Common.Aspire/Extensions.cs:169`. +5. **The 2026-08-14 anchor claim is annotated rather than removed**, consistent with how every + preceding revision treated its predecessor. diff --git a/docs-src/adr/028-dark-theme-mode.md b/docs-src/adr/028-dark-theme-mode.md index 567f255..94a00ef 100644 --- a/docs-src/adr/028-dark-theme-mode.md +++ b/docs-src/adr/028-dark-theme-mode.md @@ -21,7 +21,7 @@ intended end state but is not yet wired for theme (see Decision 3). Day/Dark lifecycle in one place. Inside that component `MudThemeProvider` is bound with `@bind-IsDarkMode` against the already-complete `MMCATheme.Instance` (`MMCA.Common.UI/Components/MmcaThemeProviders.razor:11`), a two-way binding to that component's own - `_isDarkMode` field (`MmcaThemeProviders.razor:17`); no `@ref` is used. The layout no longer holds the + `_isDarkMode` field (`MmcaThemeProviders.razor:24`); no `@ref` is used. The layout no longer holds the provider markup or the `_isDarkMode` field itself. No new palette work. 2. **A `ThemeService` (`MMCA.Common.UI`) owns the preference**, registered in `AddUIShared`. It holds the @@ -74,4 +74,3 @@ intended end state but is not yet wired for theme (see Decision 3). and is the model for the theme no-flash SSR bootstrap that is not yet wired), [ADR-022](022-browser-session-cookie-auth.md) (the SSR cookie-read pattern), [ADR-015](015-architecture-fitness-functions.md) (the host-wiring fitness assertion). -``` diff --git a/docs-src/adr/030-startup-sole-migrator.md b/docs-src/adr/030-startup-sole-migrator.md index f7605fd..a5af05c 100644 --- a/docs-src/adr/030-startup-sole-migrator.md +++ b/docs-src/adr/030-startup-sole-migrator.md @@ -24,8 +24,8 @@ in production and is the sole migrator of its own database**: it applies its pen at startup, before the new revision serves traffic. There is deliberately **no** separate deploy-step migration (no `sqlcmd` / `dotnet ef database update` apply in `deploy.yml`). -- **Set in prod for every service.** `MMCA.Store/infra/main.bicep:1031,1160,1279` (Identity/Catalog/Sales) - and `MMCA.ADC/infra/main.bicep:1106,1285,1404,1547` (Identity/Conference/Engagement/Notification) all set +- **Set in prod for every service.** `MMCA.Store/infra/main.bicep:1043,1184,1309` (Identity/Catalog/Sales) + and `MMCA.ADC/infra/main.bicep:1118,1307,1431,1579` (Identity/Conference/Engagement/Notification) all set `DatabaseInitStrategy = 'Migrate'`. - **One applier per revision.** Each service runs `minReplicas: 1`, so the startup `MigrateAsync` is not racing sibling replicas of the same revision. (Since the 2026-07-19 outbox lease revision, ADR-003, @@ -33,7 +33,7 @@ migration (no `sqlcmd` / `dotnet ef database update` apply in `deploy.yml`). is scale-out safe by construction, so above one replica the setting is a cost/migration choice.) - **No deploy-step backstop, on purpose.** Both `deploy.yml` files carry an explicit comment that there is *no external `sqlcmd` migration backstop* and that each service is the **sole migrator** - (`MMCA.Store/.github/workflows/deploy.yml:1037-1045`, `MMCA.ADC/.github/workflows/deploy.yml:1079-1087`). The + (`MMCA.Store/.github/workflows/deploy.yml:1043-1051`, `MMCA.ADC/.github/workflows/deploy.yml:1079-1088`). The `sqlcmd` that *is* installed in the pipeline is a connectivity/readiness probe, not a migration apply. - **Build-time drift gate, not a runtime apply.** CI runs `dotnet ef migrations has-pending-model-changes` (Store `deploy.yml:226`, ADC `deploy.yml:272`) so a @@ -90,12 +90,12 @@ trade-off it carries. built only for enabled modules at `:133-136`). Nothing on that path consults the hosting environment, and the service hosts call the extension method straight after `builder.Build()` (`MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:319`, - `MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:389`, - `MMCA.ADC/Source/Services/MMCA.ADC.Engagement.Service/Program.cs:325`, - `MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:255`; + `MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:396`, + `MMCA.ADC/Source/Services/MMCA.ADC.Engagement.Service/Program.cs:331`, + `MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:261`; `MMCA.Store/Source/Services/MMCA.Store.Identity.Service/Program.cs:254`, - `MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:263`, - `MMCA.Store/Source/Services/MMCA.Store.Sales.Service/Program.cs:272`), so production re-seeds on + `MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:265`, + `MMCA.Store/Source/Services/MMCA.Store.Sales.Service/Program.cs:274`), so production re-seeds on every revision. 2. **Idempotency is delegated entirely to each seeder.** There is no framework-side ledger for seed data: no `__EFMigrationsHistory` equivalent, no marker row, no "already seeded" flag. The loop diff --git a/docs-src/adr/032-password-hashing.md b/docs-src/adr/032-password-hashing.md index e4a3fd3..1fcf9b8 100644 --- a/docs-src/adr/032-password-hashing.md +++ b/docs-src/adr/032-password-hashing.md @@ -1,7 +1,8 @@ # ADR-032: Password Hashing (PBKDF2-HMAC-SHA512) with Legacy-Hash Backward Compatibility ## Status -Accepted (2026-06-29, adoption note revised 2026-07-06, registration note revised 2026-08-01). +Accepted (2026-06-29, adoption note revised 2026-07-06, registration note revised 2026-08-01, +call-site hoist recorded 2026-08-23). ## Context Identity stores a credential as a (salt, hash) pair, never plaintext. The framework needs one @@ -21,8 +22,8 @@ factor, and comparison are decided once and not re-implemented per app. Two forc Provide a single `IPasswordHasher` (`MMCA.Common.Application.Interfaces.Infrastructure`, `IPasswordHasher.cs:6`) with one implementation `PasswordHasher` (`Source/Core/MMCA.Common.Infrastructure/Services/PasswordHasher.cs:12`), registered as a singleton via -`TryAddSingleton` (`Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:462`, in the -`AddServices` helper that `AddInfrastructure` calls at `DependencyInjection.cs:154`). The call site +`TryAddSingleton` (`Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:475`, in the +`AddServices` helper that `AddInfrastructure` calls at `DependencyInjection.cs:167`). The call site always runs, but `TryAdd` semantics mean a host that has already registered its own `IPasswordHasher` keeps it: the framework supplies the default hasher rather than forcing it. The type is stateless (only `const` parameters), so @@ -50,7 +51,7 @@ the singleton lifetime is safe. - **Comparison is constant time.** Both paths compare the recomputed bytes to the stored hash with `CryptographicOperations.FixedTimeEquals` (`PasswordHasher.cs:58`), which always reads the full length so the verify time does not leak how many leading bytes matched. -- **Adopted by both apps' Identity flow, through a shared base.** Each app's `AuthenticationService` is now a +- **Adopted by both apps' Identity flow, through shared bases.** Each app's `AuthenticationService` is now a sealed subclass of `AuthenticationServiceBase` (`Source/Core/MMCA.Common.Application/Auth/AuthenticationServiceBase.cs:34`) that passes `IPasswordHasher` into the base constructor rather than calling the hasher itself. The login-time `VerifyPassword` @@ -59,13 +60,21 @@ the singleton lifetime is safe. declares the `IPasswordHasher` parameter and forwards it to the base (`MMCA.ADC/.../Identity.Application/Users/AuthenticationService.cs:38`, forwarded at `AuthenticationService.cs:43`); Store's subclass does the same (`MMCA.Store/.../Identity.Application/Users/AuthenticationService.cs:23`, - forwarded at `AuthenticationService.cs:30`). A handful of use cases still inject `IPasswordHasher` directly - rather than through the base: both apps' `ChangePasswordHandler` - (`MMCA.ADC` `.../UseCases/ChangePassword/ChangePasswordHandler.cs:19`, `MMCA.Store` - `.../UseCases/ChangePassword/ChangePasswordHandler.cs:18`) verify the current - password before hashing the new one, and both apps' `IdentityModuleDbSeeder` - (`MMCA.ADC` `.../Persistence/DbContexts/Seeding/IdentityModuleDbSeeder.cs:29`, `MMCA.Store` - `.../Persistence/DbContexts/Seeding/IdentityModuleDbSeeder.cs:24`) hash the seeded accounts' passwords. + forwarded at `AuthenticationService.cs:30`). The other two hasher call sites are hoisted the same way, each + into its own shared base. The change-password workflow lives in + `ChangePasswordHandlerBase` (`MMCA.Common.Application.Users.UseCases.ChangePassword`, + `ChangePasswordHandlerBase.cs:24`), which verifies the current password (`ChangePasswordHandlerBase.cs:55`) + before hashing the new one (`ChangePasswordHandlerBase.cs:61`); each app's `ChangePasswordHandler` is a sealed + subclass that only declares the `IPasswordHasher` parameter and forwards it (`MMCA.ADC` + `.../UseCases/ChangePassword/ChangePasswordHandler.cs:19`, `MMCA.Store` + `.../UseCases/ChangePassword/ChangePasswordHandler.cs:18`). Seeding lives in + `IdentityModuleDbSeederBase` (`MMCA.Common.Infrastructure.Persistence.DbContexts.Seeding`, + `IdentityModuleDbSeederBase.cs:38`), which hashes each seeded account's password + (`IdentityModuleDbSeederBase.cs:103`); each app's `IdentityModuleDbSeeder` supplies only the account list and + forwards `IPasswordHasher` to the base (`MMCA.ADC` + `.../Persistence/DbContexts/Seeding/IdentityModuleDbSeeder.cs:29`, `MMCA.Store` + `.../Persistence/DbContexts/Seeding/IdentityModuleDbSeeder.cs:24`). Every hasher invocation both apps depend + on therefore sits in framework code; the app code only injects the dependency and passes it down. ## Rationale - **One framework-owned primitive, not per-app crypto.** Putting the algorithm, work factor, salt size, @@ -106,5 +115,31 @@ ADR-005 (soft-delete vs erasure: the same Infrastructure layer's `EncryptedStrin sensitive columns, the at-rest counterpart to hashing credentials), ADR-029 (authentication brute-force protection: lockout and throttling wrap the same login path whose final credential check is this hasher). -This ADR supersedes the one-line "Password hashing" note in `SECURITY.md:25` with a governance record; +This ADR supersedes the one-line "Password hashing" note in `SECURITY.md:29` with a governance record; the security model summary there stays as the reader-facing pointer. + +## Revision (2026-08-23) +**The decision is unchanged: one framework-owned `IPasswordHasher`, PBKDF2-HMAC-SHA512 for new passwords, +salt-length dispatch on verification.** What changed is where the last two call sites live. The two use cases +this record used to list as exceptions to the base-hoisting pattern (`ChangePasswordHandler` and +`IdentityModuleDbSeeder`) have since been hoisted the same way `AuthenticationService` was, into their own +shared Common bases. + +1. **Change-password.** The verify-then-hash workflow lives once in `ChangePasswordHandlerBase` + (`MMCA.Common/Source/Core/MMCA.Common.Application/Users/UseCases/ChangePassword/ChangePasswordHandlerBase.cs:24`): + `VerifyPassword` against the stored hash and salt at `:55`, `HashPassword` for the new credential at `:61`, + and the aggregate's own `ChangePassword` invariant at `:62`. The per-app classes remain as sealed subclasses + with no body of their own (ADC `ChangePasswordHandler.cs:17-23`, Store `ChangePasswordHandler.cs:16-20`), + deliberately keeping their names: `HandlerName` defaults to the runtime type name + (`ChangePasswordHandlerBase.cs:39`), so the `source` reported on every error stays the + `ChangePasswordHandler` string clients already match on. +2. **Seeding.** The seed hash call lives once in `IdentityModuleDbSeederBase` + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/Seeding/IdentityModuleDbSeederBase.cs:38`, + hashing at `:103`). The per-app seeders now supply data rather than behavior: an `Accounts` list plus a + `CreateUser` factory (abstract at `IdentityModuleDbSeederBase.cs:90`), forwarding `IPasswordHasher` to the base + constructor (ADC `IdentityModuleDbSeeder.cs:27-30`, Store `IdentityModuleDbSeeder.cs:22-25`). + +The effect on this ADR's guarantees: the "one framework-owned primitive" rationale now holds for the whole +Identity credential surface, not just login and registration. No file under either app's `Source/` invokes +`IPasswordHasher` at all; both apps only inject it and pass it to a Common base, so a future hardening (a +higher work factor, a third format) reaches every credential path in both apps through the framework alone. diff --git a/docs-src/adr/033-resource-ownership-authorization.md b/docs-src/adr/033-resource-ownership-authorization.md index 1f52060..0cb1621 100644 --- a/docs-src/adr/033-resource-ownership-authorization.md +++ b/docs-src/adr/033-resource-ownership-authorization.md @@ -96,7 +96,7 @@ ownership specification scopes list/get queries: which filters by `Id` because a cart is keyed by customer, `ShoppingCartByCustomerSpecification.cs:24`) and `OrdersController` builds an `OrdersByCustomerSpecification` through a private `GetOwnershipSpecification()` method -(`MMCA.Store/.../Sales.API/Controllers/OrdersController.cs:63-65`, +(`MMCA.Store/.../Sales.API/Controllers/OrdersController.cs:64-66`, `MMCA.Store/.../Sales.Application/Orders/Specifications/OrdersByCustomerSpecification.cs:13`, filtering by `CustomerId`, `OrdersByCustomerSpecification.cs:18`), passed as `specification: GetOwnershipSpecification()` into each query (`OrdersController.cs:105`, `OrdersController.cs:143`, diff --git a/docs-src/adr/037-field-level-encryption-at-rest.md b/docs-src/adr/037-field-level-encryption-at-rest.md index 05b4208..23e878f 100644 --- a/docs-src/adr/037-field-level-encryption-at-rest.md +++ b/docs-src/adr/037-field-level-encryption-at-rest.md @@ -18,7 +18,7 @@ never recovered. Erasure (ADR-005) **removes or anonymizes** personal data on a Neither covers the case of a field that must stay **retrievable in plaintext to the application** yet be **unreadable in the database itself**: an at-rest, reversible, column-level confidentiality control. ADR-005 already names this converter as the mechanism for erasure fields "that must remain retrievable" -(`ADRs/005-soft-delete-vs-erasure.md:17`), and `SECURITY.md:26` lists it in the security model, so the +(`ADRs/005-soft-delete-vs-erasure.md:17`), and `SECURITY.md:35` lists it in the security model, so the capability is referenced across the docs but was never recorded as a decision. The framework ships the plumbing for this, an EF Core value converter that encrypts and decrypts string @@ -220,7 +220,7 @@ mechanism for erasure fields that must stay retrievable, `ADRs/005-soft-delete-v (password hashing: the one-way credential counterpart in the same Infrastructure layer, which already calls this converter "the at-rest counterpart to hashing credentials"), ADR-018 (polyglot persistence: the shipped, tested, but unadopted precedent this record mirrors). This ADR backs the one-line "Field encryption" -entry in the security model (`SECURITY.md:26`), which stays as the reader-facing pointer. +entry in the security model (`SECURITY.md:35`), which stays as the reader-facing pointer. ## Revision (2026-07-24) Documented a constraint the converter always had but did not state: **the ciphertext is @@ -287,6 +287,6 @@ of the caller's dictionary (`:281`), and the version byte the single-key constru The work this revision documents landed via MMCA.Common PR #247 and is included in v1.153.0 (tagged 2026-08-18), so the versioned envelope and key ring are now in published packages. It is included -rather than featured: the `[1.153.0]` changelog entry (`MMCA.Common/CHANGELOG.md:63-68`) does not name +rather than featured: the `[1.153.0]` changelog entry (`MMCA.Common/CHANGELOG.md:242`) does not name the converter, which is in the release because it merged to `main` before the tag. Adoption is unchanged at zero: no entity configuration in any of the four repositories wires the converter. diff --git a/docs-src/adr/038-supply-chain-provenance.md b/docs-src/adr/038-supply-chain-provenance.md index 29ee5de..1a2a6ee 100644 --- a/docs-src/adr/038-supply-chain-provenance.md +++ b/docs-src/adr/038-supply-chain-provenance.md @@ -18,7 +18,7 @@ MassTransit-v8 license pin), but it deliberately stops there. The four controls establish provenance and integrity, an SBOM release gate, committed lock files, a CI vulnerability audit, and package source mapping, already live in the release and CI workflows, `Directory.Build.props`, and `nuget.config`, and are summarized for consumers in SECURITY.md -(SECURITY.md:33-42). No ADR owned them as a single coherent posture. This record does, and it exceeds +(SECURITY.md:42-51). No ADR owned them as a single coherent posture. This record does, and it exceeds ADR-016's scope: ADR-016 gestures at lock files as sweep mechanics, this ADR owns supply-chain integrity as the decision. @@ -106,5 +106,5 @@ governance from versioning and licensing into supply-chain provenance and integr (architecture invariants enforced as build-gating fitness functions; the same gate-the-build posture applied here to dependencies), ADR-010 (integration-event schema versioning; another release-discipline control that turns a contract into an enforced signal). See SECURITY.md ("Dependency & supply-chain -security", SECURITY.md:33-42) for the consumer-facing summary and rubric §32 +security", SECURITY.md:42-51) for the consumer-facing summary and rubric §32 (ArchitectureEvaluationCriteria.md:799-817) for the evaluation criteria. diff --git a/docs-src/adr/040-authenticated-output-caching-for-public-reads.md b/docs-src/adr/040-authenticated-output-caching-for-public-reads.md index 6658763..479fa12 100644 --- a/docs-src/adr/040-authenticated-output-caching-for-public-reads.md +++ b/docs-src/adr/040-authenticated-output-caching-for-public-reads.md @@ -29,7 +29,7 @@ and every Store Catalog policy uses it (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:153-158`), but it is a default, not a rule: ADC runs two 60-second policies whose payload cannot wait five minutes (`NowNextCache`, a clock-dependent now-and-next snapshot, and `BookmarkCountsCache`, written by another service; -`MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:251,263`). +`MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:252,264`). That design was silently inert for the traffic that matters. The shared UI HttpClient pipeline attaches the stored Bearer token to every outgoing API request via `AuthDelegatingHandler`, @@ -153,11 +153,11 @@ and not every write path evicts tags, so a cached stale row version makes the ne exactly that: `UserSessionBookmarkCacheEvictionHandler` (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandler.cs:43`, raising the event at `:77`) with the Conference host consuming it (`AddOutputCacheEvictionHandler()` - at `MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:269`, - `RegisterOutputCacheEvictionConsumer()` at `:366`), keeping the short TTL as the backstop for a lost - or late event rather than the only defense (`BookmarkCountsCache`, 60 seconds, `Program.cs:263`). + at `MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:270`, + `RegisterOutputCacheEvictionConsumer()` at `:373`), keeping the short TTL as the backstop for a lost + or late event rather than the only defense (`BookmarkCountsCache`, 60 seconds, `Program.cs:264`). A payload that changes on the clock still has no mutation to evict on, so a short TTL remains its - whole answer (`NowNextCache`, 60 seconds, `Program.cs:251`). When adding a cached endpoint, check + whole answer (`NowNextCache`, 60 seconds, `Program.cs:252`). When adding a cached endpoint, check which process owns every write that can change its payload, and whether time alone changes it. - Cache hit rate becomes meaningful for authenticated load tests; k6 scripts that log in now exercise the same cache path as anonymous ones. diff --git a/docs-src/adr/046-http-api-versioning.md b/docs-src/adr/046-http-api-versioning.md index 00049c7..a559a20 100644 --- a/docs-src/adr/046-http-api-versioning.md +++ b/docs-src/adr/046-http-api-versioning.md @@ -31,30 +31,30 @@ host through a single registration call, and keep it exercised by a shared fitne proves two live versions coexist. - **One registration wires the whole policy.** `AddCommonApiVersioning` - (`Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs:233`) deliberately + (`Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs:243`) deliberately does **not** set `DefaultApiVersion`: `1.0` is already the `Asp.Versioning` library default, and the API explorer inherits both it and `AssumeDefaultVersionWhenUnspecified` from the versioning options, so restating either one trips AV0011/AV0024. The code comment recording that omission is at - `WebApplicationBuilderExtensions.cs:235`-`WebApplicationBuilderExtensions.cs:237`. What the registration does set: it assumes the default + `WebApplicationBuilderExtensions.cs:245`-`WebApplicationBuilderExtensions.cs:247`. What the registration does set: it assumes the default version when a caller sends no header - (`AssumeDefaultVersionWhenUnspecified = true`, `WebApplicationBuilderExtensions.cs:240`), reports + (`AssumeDefaultVersionWhenUnspecified = true`, `WebApplicationBuilderExtensions.cs:250`), reports the supported/deprecated versions on every response (`ReportApiVersions = true`, - `WebApplicationBuilderExtensions.cs:241`), and selects the version from an `api-version` request - header (`new HeaderApiVersionReader("api-version")`, `WebApplicationBuilderExtensions.cs:242`). The + `WebApplicationBuilderExtensions.cs:251`), and selects the version from an `api-version` request + header (`new HeaderApiVersionReader("api-version")`, `WebApplicationBuilderExtensions.cs:252`). The reader is header-based deliberately: routes and query strings stay version-free, so a caller opts into a newer shape by adding one header rather than changing the URL. - **The API explorer is wired for versioned OpenAPI.** The same call chains `.AddMvc()` then - `.AddApiExplorer` (`WebApplicationBuilderExtensions.cs:243`, - `WebApplicationBuilderExtensions.cs:244`), formatting version groups as `'v'VVV` - (`WebApplicationBuilderExtensions.cs:246`) and substituting the version into the URL where a host - routes one (`SubstituteApiVersionInUrl = true`, `WebApplicationBuilderExtensions.cs:247`). The + `.AddApiExplorer` (`WebApplicationBuilderExtensions.cs:253`, + `WebApplicationBuilderExtensions.cs:254`), formatting version groups as `'v'VVV` + (`WebApplicationBuilderExtensions.cs:256`) and substituting the version into the URL where a host + routes one (`SubstituteApiVersionInUrl = true`, `WebApplicationBuilderExtensions.cs:257`). The explorer's default-version behavior is inherited rather than configured, exactly as the comment - above it records (`WebApplicationBuilderExtensions.cs:235`-`WebApplicationBuilderExtensions.cs:237`). That + above it records (`WebApplicationBuilderExtensions.cs:245`-`WebApplicationBuilderExtensions.cs:247`). That group format feeds the `v1` OpenAPI document `AddCommonOpenApi` registers - (`WebApplicationBuilderExtensions.cs:392`), which + (`WebApplicationBuilderExtensions.cs:402`), which `MapCommonOpenApi` serves at `/openapi/v1.json` outside Production only - (`Source/Presentation/MMCA.Common.API/Startup/OpenApiEndpointExtensions.cs:28`, - `OpenApiEndpointExtensions.cs:30`). + (`Source/Presentation/MMCA.Common.API/Startup/OpenApiEndpointExtensions.cs:34`, guarded at + `OpenApiEndpointExtensions.cs:36` and mapped at `OpenApiEndpointExtensions.cs:38`). - **A shipped exemplar proves two versions coexist.** `ServiceInfoControllerBase` (`Source/Presentation/MMCA.Common.API/Controllers/ServiceInfoControllerBase.cs:30`) serves the same `/ServiceInfo` route under two versions selected by the header: `GetV1` is mapped to `1.0` @@ -140,7 +140,7 @@ resource has yet needed to evolve its shape. show up in a copied URL or a browser address bar, so the version in play is only visible to a caller that reads request/response headers. - **The OpenAPI document is single-version and dev/CI only.** `MapCommonOpenApi` serves one `v1` - document and is a no-op in Production (`OpenApiEndpointExtensions.cs:30`), so the machine-readable + document and is a no-op in Production (`OpenApiEndpointExtensions.cs:36`), so the machine-readable contract does not yet enumerate the `2.0` discovery shape and is not a public production surface. ## Related diff --git a/docs-src/adr/047-soft-deleted-user-session-revocation.md b/docs-src/adr/047-soft-deleted-user-session-revocation.md index dddc4fa..bfbb8cd 100644 --- a/docs-src/adr/047-soft-deleted-user-session-revocation.md +++ b/docs-src/adr/047-soft-deleted-user-session-revocation.md @@ -1,7 +1,10 @@ # ADR-047: Runtime Revocation of Soft-Deleted Users' Active Sessions ## Status -Accepted (2026-07-15). +Accepted (2026-07-15). Revised 2026-08-07 (validator hoisted into a shared generic, the 30-second +constant moved, the two apps revoke at different speeds). Revised 2026-08-23: the pipeline +registration moved out of `WebApplicationExtensions` into the named-step `MiddlewarePipelineBuilder` +(ADR-079), and a tenant-resolution step (ADR-073) now sits between authentication and rate limiting. ## Context Soft-delete is the framework's default deletion model (ADR-005): `AuditableBaseEntity.Delete()` sets @@ -30,12 +33,26 @@ Add a shared-pipeline middleware, `SoftDeletedUserMiddleware` rejects an authenticated caller with HTTP 401 once the caller's account has been soft-deleted, backed by a short cache so the account-status lookup is not paid on every request. -- **It runs after authentication, before authorization.** `UseCommonMiddlewarePipeline` registers it - at `Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:109`, immediately after - `UseAuthentication` / `UseRateLimiter` - (`WebApplicationExtensions.cs:96,108`) and before `UseAuthorization` - (`WebApplicationExtensions.cs:110`), so `HttpContext.User` is already populated and the check gates - every downstream endpoint. +- **It runs after authentication, before authorization.** That position is data in a named-step + pipeline builder (ADR-079), not a hand-ordered sequence of `Use*` calls: + `UseCommonMiddlewarePipeline` + (`Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:46`) delegates to the + private `ApplyPipeline` helper (`WebApplicationExtensions.cs:138`), which seeds the framework steps + from `MiddlewarePipelineBuilder.CreateDefault()` (`WebApplicationExtensions.cs:140`; + `Source/Presentation/MMCA.Common.API/Startup/MiddlewarePipelineBuilder.cs:31-156`). This middleware + is the `SoftDeletedUserFilter` step (`MiddlewarePipelineStepNames.cs:59`), applied as + `app.UseMiddleware()` at `MiddlewarePipelineBuilder.cs:130`. Four + consecutive steps fix its place: `UseAuthentication` (`:110`), then `TenantResolutionMiddleware` + (`:118`, ADR-073, which has to sit immediately after authentication because its claim strategy + reads `HttpContext.User`), then `UseRateLimiter` (`:126`, ADR-019), then this middleware (`:130`), + and only after it `UseAuthorization` (`:134`). So `HttpContext.User` is already populated when the + check runs, and the check still gates every downstream endpoint. The order is frozen by a fitness + function rather than by a comment: `MiddlewarePipelineOrderTestsBase.ExpectedStepNames` asserts + `SoftDeletedUserFilter` between `RateLimiting` and `Authorization` + (`MMCA.Common/Source/Hosting/MMCA.Common.Testing/MiddlewarePipelineOrderTestsBase.cs:51-53`). + `MiddlewarePipelineBuilder.Build()`'s startup-validated invariants (`MiddlewarePipelineBuilder.cs:257-280`) + do not name this step, so a host that moves it through the configure overload is caught by that + fitness function, not at startup. - **Anonymous requests pass straight through.** When `ICurrentUserService.UserId` is null the middleware calls the next delegate and returns without any lookup (`SoftDeletedUserMiddleware.cs:65-73`), so unauthenticated traffic pays nothing. @@ -83,7 +100,7 @@ by a short cache so the account-status lookup is not paid on every request. (`SoftDeletedUserMiddleware.cs:76-83`): Identity is the source of truth and already validated the token upstream. Resolving it as a constructor/parameter dependency would instead 500 every request in those services. MMCA.Helpdesk wires the same pipeline - (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:111`) but hosts only a Tickets module and + (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:117`) but hosts only a Tickets module and registers no validator, so it takes the same no-op path. `SoftDeletedUserMiddlewareTests` @@ -161,7 +178,11 @@ passive bound Store lives with. ADR-005 (soft-delete is the deletion model whose still-authenticated tokens this middleware revokes; deleting a user is a soft-delete, not a row removal), ADR-004 (the stateless RS256/JWKS validation that has no built-in revocation, which this bounds without a per-request store lookup), ADR-022 (the -SSR session cookie carries the same JWT into the prerender path this middleware also gates). +SSR session cookie carries the same JWT into the prerender path this middleware also gates), ADR-079 +(the shared named-step HTTP pipeline that now owns this middleware's registration and its order), +ADR-073 (multi-tenancy, whose `TenantResolutionMiddleware` step sits between authentication and the +rate limiter, ahead of this one), ADR-019 (the rate limiter that runs immediately before it, for its +own authentication-dependent reason). ## Revision (2026-08-07) Re-verified against current source. The decision is unchanged, but three things it described have @@ -205,7 +226,52 @@ revoke at the same speed. cache read at `:91` (from `:63-66`), the cached-`true` 401 at `:102-106` (from `:66-71`), the cache-miss validator call at `:114-116` with the cache write at `:131-133` and the deleted-401 at `:143-147` (from `:73-84`), and the fall-through at `:150` (from `:86`). Unchanged and re-checked: - the pipeline registration (`WebApplicationExtensions.cs:109`, with `:96,108` and `:110` around it), - `ISoftDeletedUserValidator.cs:7,15`, the Helpdesk no-op path - (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:111`), and + the pipeline registration (`WebApplicationExtensions.cs:109`, with `:96,108` and `:110` around it, + both superseded on 2026-08-23, see below), `ISoftDeletedUserValidator.cs:7,15`, the Helpdesk no-op + path (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:111`, superseded below), and + `MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Middleware/SoftDeletedUserMiddlewareTests.cs`. + +## Revision (2026-08-23) +Re-verified against current source. The decision, the cache design, the fail-open policy and the +per-app asymmetry are all unchanged; what moved is where the middleware's position is expressed, and +one new neighbour now sits ahead of it. + +1. **The registration left `WebApplicationExtensions.cs` entirely.** That file no longer calls + `UseAuthentication`, `UseRateLimiter`, `UseMiddleware` or + `UseAuthorization` at all: `UseCommonMiddlewarePipeline` (`WebApplicationExtensions.cs:46`, and the + configure overload at `:58`) only routes into the private `ApplyPipeline` helper (`:138`), which + seeds `MiddlewarePipelineBuilder.CreateDefault()` (`:140`). Every `:96`, `:108`, `:109` and `:110` + anchor the Decision and the 2026-08-07 revision carried is therefore dead. The step registrations + now live in `MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/MiddlewarePipelineBuilder.cs`: + `UseAuthentication` at `:110`, `UseRateLimiter` at `:126`, + `UseMiddleware()` at `:130`, `UseAuthorization` at `:134`. The behavior + is what it was; the order became data in a named-step builder (ADR-079). +2. **A tenant-resolution step now runs between authentication and rate limiting.** + `TenantResolutionMiddleware` is applied at `MiddlewarePipelineBuilder.cs:118` under the + `TenantResolution` step name (ADR-073), so the current run of steps is Authentication (`:110`), + TenantResolution (`:118`), RateLimiting (`:126`), SoftDeletedUserFilter (`:130`), Authorization + (`:134`). The ADR's own claim, that the check runs after authentication and before authorization, + still holds exactly; it is simply no longer adjacent to `UseAuthentication`. That new step follows + the same opt-in, inert-unless-configured posture this middleware established, and its own comment + names this middleware as the precedent (`MiddlewarePipelineBuilder.cs:114-117`). +3. **The order is now held by a fitness function.** + `MiddlewarePipelineOrderTestsBase.ExpectedStepNames` + (`MMCA.Common/Source/Hosting/MMCA.Common.Testing/MiddlewarePipelineOrderTestsBase.cs:38-58`) lists + `SoftDeletedUserFilter` between `RateLimiting` and `Authorization` (`:51-53`), so a reorder fails a + test. Note the limit: `MiddlewarePipelineBuilder.Build()` validates four load-bearing adjacencies at + startup (`MiddlewarePipelineBuilder.cs:257-280`) and none of them names this step, so a host that + moves it with the configure overload gets no startup error, only the fitness-function failure. +4. **Helpdesk anchor corrected.** `app.UseCommonMiddlewarePipeline()` is at + `MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:117` (the previously cited `:111` is now + `var app = builder.Build();`). The substance is unchanged: Helpdesk wires the shared pipeline, + registers no `ISoftDeletedUserValidator`, and takes the no-op path. +5. **Re-checked and unchanged**: `SoftDeletedUserMiddleware.cs:31` (class declaration), `:65-73` + (anonymous pass-through), `:75` and `:76-83` (lazy resolution, no-validator pass-through), `:85`, + `:91`, `:102-106`, `:114-116`, `:131-133`, `:143-147`, `:150` (the cache/validator path), `:93-100`, + `:118-125`, `:135-140` (fail-open handlers), `ISoftDeletedUserValidator.cs:7,15`, + `SoftDeletedUserValidator.cs:19-34` with its constraint at `:20` and query at `:30-33`, + `SoftDeletedUserCache.cs:29,42-43,53-61`, the two registrations + (`MMCA.ADC.Identity.Application/DependencyInjection.cs:35`, + `MMCA.Store.Identity.Application/DependencyInjection.cs:41`), the two delete handlers + (ADC `DeleteUserHandler.cs:68-80`, Store `DeleteUserHandler.cs:35-60`), and `MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/Middleware/SoftDeletedUserMiddlewareTests.cs`. diff --git a/docs-src/adr/048-primitive-identifier-type-aliases.md b/docs-src/adr/048-primitive-identifier-type-aliases.md index eb8b65d..b9a2413 100644 --- a/docs-src/adr/048-primitive-identifier-type-aliases.md +++ b/docs-src/adr/048-primitive-identifier-type-aliases.md @@ -9,6 +9,8 @@ evidence and deleted the folders themselves: being empty, they were untracked by every fresh clone, so the deferral now rests on the verifiable absence of any wrapper-struct type). Revised 2026-08-14 (Conference's alias file now declares sixteen aliases, `SponsorIdentifierType` having been added, and the ADC `User` source citations were re-anchored after an expanded doc comment). +Revised 2026-08-23 (Conference's alias file now declares seventeen aliases, `ActivityIdentifierType` +having been added, and the workspace alias count was recounted; see the Revision (2026-08-23) at the end). **Revisited by [ADR-085](085-identifier-type-aliases-revisited.md) (2026-08-18)**: the wrapper-struct alternative this record deferred was re-evaluated, priced, and deferred again, now against named revisit triggers instead of open-endedly. The decision below is unchanged; see the Revision @@ -48,13 +50,13 @@ not as a wrapper struct. app supplying the concrete `User` entity that satisfies it. Consumers follow the same pattern: ADC Identity (`MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/MMCA.ADC.Identity.GlobalUsings.IdentifierType.cs:2`), - ADC Conference with sixteen aliases - (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:5-20`), + ADC Conference with seventeen aliases + (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:5-21`), and Store Catalog (`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/MMCA.Store.Catalog.GlobalUsings.IdentifierType.cs:3-6`). - **The alias is the type; there is no wrapping struct.** The right-hand side is a bare primitive. Most resolve to `int`; the one deviation in Conference is - `SpeakerIdentifierType = System.Guid` (line 18), because Sessionize assigns speakers GUIDs while its + `SpeakerIdentifierType = System.Guid` (line 19), because Sessionize assigns speakers GUIDs while its other imported entities carry integer IDs (the file header comment records this, `MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:1-4`). - **Aliases are linked solution-wide via `Directory.Build.props`.** Each `GlobalUsings.*.cs` file is @@ -124,9 +126,10 @@ deferral it records. The last Trade-offs entry above ("Revisiting the trade would be a broad change") described the wrapper-struct migration as expensive without ever measuring it, and the Decision's last bullet left the alternative "deliberately deferred, not planned" with no condition that would re-open it. -[ADR-085](085-identifier-type-aliases-revisited.md) closes both gaps: it counts the aliases (43 across -10 files in the four repositories, 42 of them resolving to `int`), counts the migration surface -(3,641 occurrences of the alias token across 1,016 files in the four `Source` trees, tests excluded), +[ADR-085](085-identifier-type-aliases-revisited.md) closes both gaps: it counts the aliases (44 across +10 files in the four repositories, 43 of them resolving to `int`), counts the migration surface +(3,192 occurrences of the alias token across 1,001 `.cs`/`.razor` files in the four `Source` trees, +tests excluded, `TIdentifierType` generic parameters not counted), names the concrete failure the deferral leaves open with a live example (ADC's `CheckIn` constructor, which takes two different `UserIdentifierType` arguments that can be transposed silently), and records three triggers that would re-open the question: a production defect traced to an identifier @@ -135,3 +138,32 @@ graph. Read the two records together as one position: this record is the decision and its evidence, ADR-085 is its price and its expiry condition. + +## Revision (2026-08-23) +No decision and no rationale changed. Two counts did, both because Conference gained an alias. + +Conference's alias file declares **seventeen** aliases, `ActivityIdentifierType = int` having been +added at the head of the list +(`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:5-21`), +which also moved `SpeakerIdentifierType = System.Guid` down to line 19 (same file). The Decision +section above is re-anchored to both. + +Recounting the alias files across the four repositories gives **44 aliases across 10 files**: Common +Domain 1 (`MMCA.Common/Source/Core/MMCA.Common.Domain/GlobalUsings.IdentifierType.cs:1`) and Common +Shared 2 (`MMCA.Common/Source/Core/MMCA.Common.Shared/GlobalUsings.NotificationIdentifierType.cs:1-2`); +ADC Notification 2 +(`MMCA.ADC/Source/Modules/Notification/MMCA.ADC.Notification.GlobalUsings.IdentifierType.cs:1-2`), +Identity 1 +(`MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Shared/MMCA.ADC.Identity.GlobalUsings.IdentifierType.cs:2`), +Engagement 10 +(`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Shared/MMCA.ADC.Engagement.GlobalUsings.IdentifierType.cs:4-13`) +and Conference 17; Store Catalog 4 +(`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Shared/MMCA.Store.Catalog.GlobalUsings.IdentifierType.cs:3-6`), +Identity 2 +(`MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Shared/MMCA.Store.Identity.GlobalUsings.IdentifierType.cs:3-4`) +and Sales 3 +(`MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.Shared/MMCA.Store.Sales.GlobalUsings.IdentifierType.cs:5-7`); +and Helpdesk Tickets 2 +(`MMCA.Helpdesk/Source/Modules/Tickets/MMCA.Helpdesk.Tickets.Shared/MMCA.Helpdesk.Tickets.GlobalUsings.IdentifierType.cs:6,8`). +43 of the 44 resolve to `int`; `SpeakerIdentifierType` remains the single `Guid`. The count in the +Revision (2026-08-18) above is refreshed to those numbers, and ADR-085 carries the same pair. diff --git a/docs-src/adr/049-library-configureawait-policy.md b/docs-src/adr/049-library-configureawait-policy.md index ad093a0..d3c510b 100644 --- a/docs-src/adr/049-library-configureawait-policy.md +++ b/docs-src/adr/049-library-configureawait-policy.md @@ -1,7 +1,7 @@ # ADR-049: Library-Scoped ConfigureAwait(false) Policy (CA2007) ## Status -Accepted (2026-07-20; measurements re-anchored 2026-08-07, 2026-08-14 and 2026-08-18). +Accepted (2026-07-20; measurements re-anchored 2026-08-07, 2026-08-14, 2026-08-18 and 2026-08-23). ## Context MMCA.Common ships as NuGet packages consumed by host applications, not as an application itself. @@ -46,8 +46,8 @@ application code do not. - **Standard .NET library guidance, applied at the boundary where it holds.** The rule is scoped to exactly the code that ships in packages; it is not blanket-applied to the apps, where it would be 360+ sites of pure noise (measured across Store/ADC before this decision, and the scale has only - grown: a raw `\bawait\b` scan on 2026-08-18 counts 616 occurrences in `MMCA.Store/Source` and 1,386 - in `MMCA.ADC/Source`, 2,002 combined, which is the upper bound on the CA2007 sites the rule would + grown: a raw `\bawait\b` scan on 2026-08-23 counts 617 occurrences in `MMCA.Store/Source` and 1,462 + in `MMCA.ADC/Source`, 2,079 combined, which is the upper bound on the CA2007 sites the rule would open there). - **Mechanical, with the enforcement and the remediation at different levels.** The build gate is the enforced half: a new context-capturing await in packaged non-UI code fails the build, so it costs no @@ -57,8 +57,8 @@ application code do not. ## Trade-offs - **Visual noise in framework source.** Every await in `Source/` (except UI packages) carries - `.ConfigureAwait(false)` (324 sites at adoption; 718 gated sites as of the 2026-08-18 snapshot, out - of 811 across `Source/` once the exempt UI packages are counted back in). The gate makes it uniform, + `.ConfigureAwait(false)` (324 sites at adoption; 767 gated sites as of the 2026-08-23 snapshot, out + of 860 across `Source/` once the exempt UI packages are counted back in). The gate makes it uniform, so the noise is consistent rather than sporadic. - **A per-repo delta in an otherwise shared analyzer baseline.** The workspace keeps one byte-identical `.editorconfig` baseline across the four repos; this policy lives in the marked @@ -162,3 +162,37 @@ exemption are unchanged; two of the three counted figures moved. with `CodeAnalysisTreatWarningsAsErrors` at `:13`, all still hold. The statement that no repo artifact invokes `dotnet format analyzers --diagnostics CA2007` was not re-searched in this pass; it carries forward from the 2026-08-07 revision that established it. + +## Revision (2026-08-23) +A re-measurement only, in the same terms as the 2026-08-18 pass. The policy, the gate and the +exemption are unchanged; both counted figures moved. + +1. **Framework site counts, measured 2026-08-23.** `MMCA.Common/Source/**/*.cs` now holds 860 + `ConfigureAwait(false)` occurrences across 176 files, of which 93 sit inside the exempt UI + packages (`MMCA.Common.UI` 49 across 15 files, `MMCA.Common.UI.Maui` 42 across 16 files, + `MMCA.Common.UI.Web` 2 in 1 file), leaving 767 under the gate. The exempt split is unchanged from + both 2026-08-14 and 2026-08-18, so all 49 new occurrences (and all 8 new files) landed in gated + code, which is what a working gate looks like: every await added to packaged non-UI code in the + last five days carries the call. The 2026-08-18 figures (811 / 168 files, 93 exempt, 718 gated), + the 2026-08-14 figures (786 / 158 files, 93 exempt, 693 gated) and the 2026-08-07 figures + (719 / 147 files, 90 exempt, 629 gated) stay in their own revisions as the history of those + measurements; the Trade-offs entry now carries today's numbers. "324 sites at adoption" remains + the 2026-07-20 snapshot and is unchanged. +2. **Consumer-scale upper bound, measured 2026-08-23.** A raw `\bawait\b` scan gives 617 occurrences + across 119 files in `MMCA.Store/Source/**/*.cs` (up from 616 across 118 files: Store is + effectively flat) and 1,462 across 272 files in `MMCA.ADC/Source/**/*.cs` (up from 1,386 across + 261 files), 2,079 combined. ADC again accounts for essentially all of the growth. Raw `await` + still overcounts CA2007 sites, so this stays an upper bound and it points the same way the + original "360+" phrasing did. +3. **The gate, the exemption and the enforcement are re-verified as written.** The `[Source/**.cs]` + gate at `warning` (`MMCA.Common/.editorconfig:828-829`), the + `[Source/Presentation/MMCA.Common.UI*/**.cs]` exemption at `none` (`:831-832`), the three packaged + `MMCA.Common.UI*` projects (including `MMCA.Common.UI.Web` at + `MMCA.Common/Source/Presentation/MMCA.Common.UI.Web/MMCA.Common.UI.Web.csproj:3` and + `ServerTokenStorageService` at + `MMCA.Common/Source/Presentation/MMCA.Common.UI.Web/Services/ServerTokenStorageService.cs:17`), + and `TreatWarningsAsErrors` (`MMCA.Common/Directory.Build.props:7`) with + `CodeAnalysisTreatWarningsAsErrors` at `:13` and CA2007 in no `NoWarn` list, all still hold. The + statement that no repo artifact invokes `dotnet format analyzers --diagnostics CA2007` was + re-checked this pass against MMCA.Common's workflow files only (no match); the broader claim + still rests on the 2026-08-07 revision that established it. diff --git a/docs-src/adr/052-background-job-execution.md b/docs-src/adr/052-background-job-execution.md index d5b7d7e..fb1a31f 100644 --- a/docs-src/adr/052-background-job-execution.md +++ b/docs-src/adr/052-background-job-execution.md @@ -1,7 +1,8 @@ # ADR-052: Background Job Execution (Bounded Queue plus Hosted Drain) ## Status -Accepted (2026-07-24). +Accepted (2026-07-24). Revised 2026-08-23 (post-commit enqueue is recorded as two patterns, not one: +see the revision at the end). ## Context Some requests trigger work that cannot run inside the request: an AI scoring pass over an event's @@ -57,10 +58,16 @@ starts an untracked `Task` from a request. - **One failure posture per drain, stated once.** The drain catches per item so one failed run cannot kill the loop, and handles shutdown cancellation separately from failure so a graceful restart is not recorded as an error. -- **Post-commit work attaches to a domain event, not to the command handler.** A handler that - enqueues inline runs while the ADR-014 transaction is still open, so a rollback leaves the queued - work describing state that never persisted. Raising a domain event and enqueuing from its handler - gets post-commit delivery from the existing deferral (ADR-003), with no extra sequencing code. +- **Post-commit work is enqueued after the write is durable, never beside it.** The failure this + rules out is enqueuing while the write can still be undone, which leaves the queued work + describing state that never persisted. Two shapes satisfy it and both are in use: + - *From a domain event handler*, which gets post-commit delivery from the existing deferral + (ADR-003) with no sequencing code in the command handler: domain-event dispatch inside a + transactional command is deferred until after the commit succeeds and dropped on rollback. + - *From the command handler itself, after `await unitOfWork.SaveChangesAsync(...)` returns*, for + commands that do not opt into the ADR-014 transaction decorator (which wraps only commands + implementing `ITransactional`, so for the rest `SaveChangesAsync` **is** the commit). The + ordering here is manual and load-bearing: the enqueue call has to stay below the save. Two implementations exist: `LiveChannelPublishQueue` / `LiveChannelPublishProcessor` (ephemeral, DropOldest, ADR-039) and `SessionScoringQueue` / `SessionScoringProcessor` (expensive, Wait, dedup @@ -100,3 +107,42 @@ ADR-008 (the broker, for cross-service work), ADR-014 (the transactional decorator whose commit boundary post-commit work attaches to), ADR-039 (live channel push, the ephemeral instance of this pattern), ADR-025 (startup warm-up, the other hosted-service use in the framework). + +## Revision (2026-08-23) +**The decision is unchanged: post-commit work is still enqueued only once the write is durable.** +What changed is the record of *how*. This ADR stated the domain-event handler as the mechanism; the +live-channel enqueue call sites split two-to-four the other way, so the decision bullet now names +both shapes. + +1. **Two call sites enqueue from an `IDomainEventHandler`.** + `LivePollVoteChangedHandler` implements `IDomainEventHandler` + (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/LivePolls/DomainEventHandlers/LivePollVoteChangedHandler.cs:41`) + and enqueues the `poll.results-changed` broadcast at `:79`; + `SessionQuestionUpvoteChangedHandler` implements + `IDomainEventHandler` + (`.../SessionQuestions/DomainEventHandlers/SessionQuestionUpvoteChangedHandler.cs:42`) and + enqueues the upvote-count broadcast at `:80`. Both are singletons that open their own scope + (`:53` and `:54` respectively) and wrap the work in `BestEffort.ExecuteAsync` (`:51`, `:52`). +2. **Four call sites enqueue from the command handler, on the line after the save.** There is no + domain event in between: + - `CloseLivePollHandler` awaits `unitOfWork.SaveChangesAsync` at + `.../LivePolls/UseCases/Close/CloseLivePollHandler.cs:70`, logs at `:72` and calls + `EnqueueClosed(poll)` at `:74`; the queue write itself is at `:95`. + - `OpenLivePollHandler` saves at `.../LivePolls/UseCases/Open/OpenLivePollHandler.cs:85` and + calls `EnqueueOpened(poll)` at `:89`, queue write at `:110`. + - `SubmitQuestionHandler` saves at + `.../SessionQuestions/UseCases/Submit/SubmitQuestionHandler.cs:103` and awaits + `EnqueueSubmittedAsync(question)` at `:107`; that helper is a `BestEffort.ExecuteAsync` body + (`:130-131`) with one queue write per branch (`:142` approved, `:157` pending-count). + - `ModerateQuestionHandler` saves at + `.../SessionQuestions/UseCases/Moderate/ModerateQuestionHandler.cs:80` and awaits + `EnqueueModeratedAsync(...)` at `:84`, queue writes at `:138` and `:152`. +3. **The safety property still holds for those four, because they are not transactional commands.** + `TransactionalCommandDecorator` wraps only commands implementing the `ITransactional` marker and + passes everything else straight through + (`MMCA.Common/Source/Core/MMCA.Common.Application/UseCases/Decorators/TransactionalCommandDecorator.cs:25-26`, + marker at `.../UseCases/ITransactional.cs:6`). No Engagement command implements it, so for these + handlers `SaveChangesAsync` is the commit and an enqueue below it is genuinely post-commit. The + difference from shape 1 is where the ordering lives: in the command handler's statement order + rather than in the ADR-003 deferral, so it is a rule a future edit to those handlers has to keep + rather than one the pipeline keeps for them. diff --git a/docs-src/adr/054-saga-compensation-and-reconciliation.md b/docs-src/adr/054-saga-compensation-and-reconciliation.md index f959695..5218016 100644 --- a/docs-src/adr/054-saga-compensation-and-reconciliation.md +++ b/docs-src/adr/054-saga-compensation-and-reconciliation.md @@ -41,7 +41,7 @@ saga-timeout backstop for steps that depend on an external system. payment is `OrderPaymentFailedSagaHandler` (`.../Orders/Saga/OrderPaymentFailedSagaHandler.cs:20-22`). A new compensating action is a new handler, not an edit to the command. - **Each handler runs in its own DI scope.** Domain-event handlers are registered as singletons - (`MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:135-140`), so every one + (`MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:143-148`), so every one opens its own scope through `IServiceScopeFactory` (`OrderCancelledSagaHandler.cs:41`, `OrderPaymentFailedSagaHandler.cs:29`, `.../Infrastructure/Services/PaymentReconciliationService.cs:89,132`), and the ones that persist @@ -162,7 +162,7 @@ adopted. is gone. - **Redelivery re-runs every handler of the event, not the failed one.** The dispatcher iterates handlers sequentially with no per-handler isolation - (`MMCA.Common/.../Services/DomainEventDispatcher.cs:55-64`), so one throwing handler also skips the + (`MMCA.Common/.../Services/DomainEventDispatcher.cs:76-85`), so one throwing handler also skips the handlers after it, and a redelivery re-runs the ones that already succeeded. The blast radius is wider than the one event: the interceptor dispatches every local event of a save in a single call and marks their outbox rows processed only afterwards @@ -173,7 +173,7 @@ adopted. swallow its failures itself. - **The sweep is not replica-leased.** The outbox processor claims rows with a lease before working them (ADR-003); the sweep takes no such claim, so at the configured `maxReplicas: 2` - (`MMCA.Store/infra/main.bicep:1309`) two replicas can pick the same stuck order and each spend a + (`MMCA.Store/infra/main.bicep:1339`) two replicas can pick the same stuck order and each spend a Stripe status call. Correctness holds through the concurrency token; the duplicated external call does not deduplicate. - **Every compensating action needs its own marker.** There is no generic mechanism: the ADR-021 diff --git a/docs-src/adr/055-repository-and-specification-contract.md b/docs-src/adr/055-repository-and-specification-contract.md index 64ed816..e66c9f8 100644 --- a/docs-src/adr/055-repository-and-specification-contract.md +++ b/docs-src/adr/055-repository-and-specification-contract.md @@ -30,6 +30,12 @@ and twelve `IEntityReader` / `IEntityQuerier` declarations to production; MMCA.S narrowing staged on an open pull request. The Decision's consumption paragraph is restated, the "ISP split is guidance" Trade-offs entry is marked superseded in place, and two sentences of the Revision (2026-08-18) carry pointer notes. See the Revision (2026-08-21) at the end). +Revised 2026-08-23 (**substantive in consequence, not in decision**: the MMCA.Store counterpart the +Revision (2026-08-21) recorded as staged on an open pull request merged to `main` on 2026-08-22, so +the narrow read interfaces now have shipped dependents in both consumer applications. The Decision's +consumption paragraph, the superseded "ISP split is guidance" Trade-offs note, and the Store +paragraph plus the adoption-sample caution in the Revision (2026-08-21) are restated to that +reality, with the Store holders anchored by file and line for the first time). ## Context Every read an application handler performs has to come from somewhere, and the shape of that contract @@ -156,9 +162,17 @@ or to `IEntityQuerier` where it projects or counts (for example `MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Users/IntegrationEventHandlers/UserRegisteredHandler.cs:131`), and locals typed to the querier where a visibility helper reads through the unit of work (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Common/PublicConferenceVisibility.cs:40`, -`:75`, `:126`, `:151`). MMCA.Store has the same narrowing staged on an open pull request (three -holders; a fourth, `ProductVariantService`, deliberately stays on `IReadRepository` because it calls -members of both halves). MMCA.Helpdesk still narrows nothing; the only mention of either name there +`:75`, `:126`, `:151`). MMCA.Store ships the same narrowing at three read-only holders: a +by-id/existence service field +(`MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Application/Customers/CustomerService.cs:14`), +a handler parameter narrowed to `IEntityReader` +(`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Application/Categories/UseCases/AssignParentCategory/AssignParentCategoryHandler.cs:68`), +and a querier parameter on the customer-provisioning handler +(`MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Application/Users/DomainEventHandlers/UserRegisteredHandler.cs:98`); +a fourth, `ProductVariantService` +(`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Application/Products/ProductVariantService.cs:15`), +deliberately stays on `IReadRepository` because it calls members of both halves. MMCA.Helpdesk still +narrows nothing; the only mention of either name there remains a pair of comments in its template staging script (`MMCA.Helpdesk/build/templates/stage.ps1:250`, `:983`) describing the contract rather than depending on it. @@ -175,7 +189,7 @@ constructor-injects `IEntityReader` or `IEntityQuerier`, and nothing should: the where an entity's physical data source resolves to the right context and where the repository instance is cached for the scope, so a narrow interface resolved straight from the container would bypass both. The ISP split is therefore the declared dependency shape at read-only call sites in two -applications, one shipped and one in review, sitting on an unchanged registration surface +shipped applications, sitting on an unchanged registration surface (`IRepository.cs:216-217`). ## Rationale @@ -202,7 +216,7 @@ applications, one shipped and one in review, sitting on an unchanged registratio ## Trade-offs - **The ISP split is guidance, not yet a wired dependency.** *(Superseded by the Revision - (2026-08-21) below: the split now has real dependents in ADC (shipped) and Store (staged), obtained + (2026-08-21) below: the split has real dependents shipped in both MMCA.ADC and MMCA.Store, obtained from the unchanged `IUnitOfWork` accessors by implicit conversion, so no change to how the repository is obtained was needed after all; kept as the record of the trade-off as originally accepted.)* Because the only accessors return the @@ -434,10 +448,12 @@ change, none of them behavioral: helpers, and the visibility helper's locals), each still assigned from the unit of work. Helpers that also write kept the composite, correctly. -**MMCA.Store has the counterpart staged** on an open pull request (CI green, held unmerged for -operational reasons unrelated to the change): three read-only holders narrowed, and -`ProductVariantService` deliberately left on `IReadRepository` because it calls `ExistsAsync` (a -reader member) and `GetProjectedAsync` (a querier member) from the same field. That refusal is the +**MMCA.Store ships the counterpart** *(merged to `main` on 2026-08-22, after this revision was +written; restated here as of 2026-08-23)*: three read-only holders narrowed +(`CustomerService.cs:14`, `AssignParentCategoryHandler.cs:68`, `UserRegisteredHandler.cs:98`), and +`ProductVariantService` deliberately left on `IReadRepository` +(`ProductVariantService.cs:15`) because it calls `ExistsAsync` (a reader member, `:20`, `:26`) and +`GetProjectedAsync` (a querier member, `:38`, `:55`) from the same field. That refusal is the split working as designed: a holder that genuinely needs both halves says so by keeping the composite. Both applications also state the convention in their CLAUDE.md files, so new read-only code is pointed at the narrow interfaces by guidance as well as by example. @@ -450,6 +466,6 @@ code is pointed at the narrow interfaces by guidance as well as by example. conversion from `GetReadRepository<,>()` is the supported acquisition path. - **Half the composition surface still has no caller.** `Or` and `Not`, the specification-taking `CountAsync` and `AnyAsync`, and `GetPageByCursorAsync` have zero application callers in any of - the four repositories, and MMCA.Helpdesk consumes none of this. The adoption sample is one - application shipped and one staged; the "untested by usage" caution above still applies to the - members nothing calls. + the four repositories, and MMCA.Helpdesk consumes none of this. The adoption sample is two + applications, both shipped; the "untested by usage" caution above still applies to the members + nothing calls. diff --git a/docs-src/adr/056-blazor-render-mode-strategy.md b/docs-src/adr/056-blazor-render-mode-strategy.md index 6803bba..45af7fc 100644 --- a/docs-src/adr/056-blazor-render-mode-strategy.md +++ b/docs-src/adr/056-blazor-render-mode-strategy.md @@ -5,7 +5,9 @@ Accepted (2026-07-28). Revised 2026-08-14: re-anchored the host, base-class and their current lines; scoped the "only `@rendermode` attributes" enumeration to the repos this ADR governs (the workspace now also holds `InteractiveServer`-only sample and workshop heads); corrected the list-page inheritor count from sixteen to eighteen; and narrowed the Helpdesk `MudTable` claim to its -list page. +list page. Revised 2026-08-23: re-anchored the ADC `App.razor`, `ADCHome`, AppHost and ADR-051 +citations to their current lines, and set the list-page inheritor count to nineteen (thirteen in ADC, +six in Store), eighteen of them routable. ## Context Both web applications are Blazor Web Apps: a static server-rendered (SSR) prerender pass produces the @@ -22,7 +24,7 @@ InteractiveWebAssembly client)" (`027-multi-locale-i18n.md:13-16`); ADR-028 repe premise for the theme and records that "there is no free no-flash for InteractiveAuto" (`028-dark-theme-mode.md:11-15`, `028-dark-theme-mode.md:64-66`); ADR-051 builds the whole client token lifecycle around three heads with three storage stories and ends with "UI code never branches on render -mode" (`051-client-auth-token-lifecycle.md:13-28`, `051-client-auth-token-lifecycle.md:34`). ADR-042 +mode" (`051-client-auth-token-lifecycle.md:13-28`, `051-client-auth-token-lifecycle.md:36`). ADR-042 covers the MAUI head and never mentions render modes at all. So four decisions depend on a render-mode policy that no ADR states, and the policy itself is only discoverable by reading two `App.razor` files, two AppHosts, two CI workflows, and one framework base class. This ADR states it. @@ -34,7 +36,7 @@ layer rather than by weakening the render mode. - **The mode is set once, at the root, on the shared router.** Each app's `App.razor` applies the same mode expression to `HeadOutlet` and to `Routes` - (`MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Components/App.razor:16`, `App.razor:20`; + (`MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Components/App.razor:41`, `App.razor:45`; `MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/Components/App.razor:17`, `App.razor:21`), and `Routes` is the single framework-owned router shared by both apps (`MMCA.Common/Source/Presentation/MMCA.Common.UI/Routes.razor:7`). **No page or component in either app @@ -46,7 +48,7 @@ layer rather than by weakening the render mode. scope and are not counted here. - **`InteractiveAuto` is the default for both web heads.** Each `App.razor` resolves an `AppRenderMode` property that returns `InteractiveAuto` unless an E2E flag is set - (`MMCA.ADC/.../App.razor:41-52`, `MMCA.Store/.../App.razor:39-42`). Both hosts register both runtimes on + (`MMCA.ADC/.../App.razor:66-77`, `MMCA.Store/.../App.razor:39-42`). Both hosts register both runtimes on both sides of the pipeline: `AddInteractiveServerComponents()` + `AddInteractiveWebAssemblyComponents()` at service registration and `AddInteractiveServerRenderMode()` + `AddInteractiveWebAssemblyRenderMode()` on `MapRazorComponents()` @@ -65,8 +67,8 @@ layer rather than by weakening the render mode. clears it instead of issuing a redundant API round-trip (`DataGridListPageBase.cs:444-458`), which the base's own comment records as the fix for the visible cancel-retry cycle caused by the SSR to Server to WASM transition (`DataGridListPageBase.cs:132-135`). The payload is a - `PersistedGridState` record of items plus total (`DataGridListPageBase.cs:805`). **Eighteen types - inherit this base** (twelve in ADC, six in Store), seventeen of them routable list pages plus ADC's + `PersistedGridState` record of items plus total (`DataGridListPageBase.cs:805`). **Nineteen types + inherit this base** (thirteen in ADC, six in Store), eighteen of them routable list pages plus ADC's non-routable Engagement `AttendeeSearchPanel` (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Pages/CheckIn/AttendeeSearchPanel.razor.cs:16`), so the policy is written once and adopted by inheritance. @@ -85,7 +87,7 @@ layer rather than by weakening the render mode. `if (!RendererInfo.IsInteractive) return;` guard in `OnParametersSetAsync` / `OnInitializedAsync` appears across both apps, with two different stated reasons: avoiding the doubled reads under `InteractiveAuto` (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.UI/Pages/Public/PublicSessionDetail.razor.cs:81-90`, - `.../Pages/Public/PublicSpeakerDetail.razor.cs:59`, `.../Pages/Home/ADCHome.razor.cs:101`, + `.../Pages/Public/PublicSpeakerDetail.razor.cs:59`, `.../Pages/Home/ADCHome.razor.cs:116`, `.../Pages/Speaker/SpeakerDashboard.razor.cs:65`, `MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Pages/SessionLive/SessionLive.razor.cs:69`, `.../Pages/SessionLive/PresenterView.razor.cs:56`) and the fact that no auth token can be read at @@ -118,10 +120,10 @@ layer rather than by weakening the render mode. (`MMCA.Common/Source/Presentation/MMCA.Common.UI.Web/Components/Pages/Error.razor:9-14`). - **`InteractiveServer` is pinned only under E2E configuration flags, never in production or local dev.** `E2E:ForceServer` returns `InteractiveServer`; ADC additionally honors `E2E:ForceWebAssembly`, which - returns `InteractiveWebAssembly` and wins if both are set (`MMCA.ADC/.../App.razor:41-52`, + returns `InteractiveWebAssembly` and wins if both are set (`MMCA.ADC/.../App.razor:66-77`, `MMCA.Store/.../App.razor:39-42`). Those config keys are injected only by the AppHosts, and only when the - matching environment variable is present (`MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:307-322`, - `MMCA.Store/Source/Hosting/MMCA.Store.AppHost/Program.cs:276-284`). In CI only `E2E_FORCE_SERVER` is + matching environment variable is present (`MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:305-320`, + `MMCA.Store/Source/Hosting/MMCA.Store.AppHost/Program.cs:286-294`). In CI only `E2E_FORCE_SERVER` is exported (`MMCA.ADC/.github/workflows/e2e.yml:218`, `MMCA.Store/.github/workflows/e2e.yml:210`); ADC's workflow deliberately does **not** set `E2E_FORCE_WASM` and records why (`MMCA.ADC/.github/workflows/e2e.yml:203-210`). Both `App.razor` comments cite the same trace evidence: @@ -147,12 +149,12 @@ layer rather than by weakening the render mode. Server circuit's immediate interactivity while the WASM bundle downloads in the background; return visits run client-side and stop consuming a server circuit. Because the mode is applied at the root router, no page has to opt in or know which runtime it is in, which is the same posture ADR-051 takes - for tokens (`051-client-auth-token-lifecycle.md:34`). + for tokens (`051-client-auth-token-lifecycle.md:36`). - **Keeping prerender is not negotiable, so the double fetch had to be fixed instead.** Prerender is the entire reason ADR-022's SSR cookie scheme exists, and it is what makes public browse pages render without waiting on a runtime boot. Disabling it would have removed the duplicate fetch by removing the feature; persisting the prerender result keeps both. -- **Encoding the policy in a base class beats documenting it.** Eighteen types inherit the +- **Encoding the policy in a base class beats documenting it.** Nineteen types inherit the persist/restore path, the explicit render-mode registration, and the bounded prerender fetch by inheriting one type; the alternative was the same 30 lines repeated per page, which is what `CatalogBrowse` shows happening the moment a page falls outside the family. diff --git a/docs-src/adr/058-runtime-conformance-suites-as-a-package.md b/docs-src/adr/058-runtime-conformance-suites-as-a-package.md index b7dacae..7290c63 100644 --- a/docs-src/adr/058-runtime-conformance-suites-as-a-package.md +++ b/docs-src/adr/058-runtime-conformance-suites-as-a-package.md @@ -1,7 +1,7 @@ # ADR-058: Runtime Conformance Suites Shipped as a Package ## Status -Accepted (2026-07-28; revised 2026-08-14 and 2026-08-18). +Accepted (2026-07-28; revised 2026-08-14, 2026-08-18, and 2026-08-23). ## Context ADR-015 turned the architecture invariants into build-gating tests, and drew its own boundary @@ -132,14 +132,20 @@ and subclasses `DecoratorPipelineOrderTestsBase` against the real Tickets pair ` `GetTicketByIdQuery` (`MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/DecoratorPipelineOrderTests.cs:30-31`), whose `ConfigureServices` runs the seed's own registration sequence: `AddApplication()`, the Tickets -module scan, then `AddApplicationDecorators()` last (`:50-52`). That file is the only one in the repo -importing the `MMCA.Common.Testing` namespace (`:10`); the package reference its domain-test project +module scan, then `AddApplicationDecorators()` last (`:50-52`). Two files in that project import the +`MMCA.Common.Testing` namespace, and only one of them belongs to this record: that decorator subclass +(`:10`). The other, `MiddlewarePipelineOrderTests.cs` (`:1`), is a one-line subclass of +`MiddlewarePipelineOrderTestsBase` +(`MMCA.Common/Source/Hosting/MMCA.Common.Testing/MiddlewarePipelineOrderTestsBase.cs:29`, +`MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/MiddlewarePipelineOrderTests.cs:15`) +guarding the ADR-079 shared HTTP edge-pipeline order, not one of the six contract bases above. +The package reference the domain-test project carries (`MMCA.Helpdesk/Tests/Modules/Tickets/MMCA.Helpdesk.Tickets.Domain.Tests/MMCA.Helpdesk.Tickets.Domain.Tests.csproj:8`) is still unused. The five HTTP-facing bases have no Helpdesk subclass, so the seed's three test projects remain the Tickets domain and application suites plus the architecture suite, which now carries both tiers: the ADR-015 structural bases from the separate `MMCA.Common.Testing.Architecture` -package (`MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/GlobalUsings.cs:3`) and -this one runtime conformance base. +package (`MMCA.Helpdesk/Tests/Architecture/MMCA.Helpdesk.Architecture.Tests/GlobalUsings.cs:3`), and +from `MMCA.Common.Testing` one base from this record's set plus the ADR-079 edge-pipeline base. ## Rationale - **Runtime conformance is the half ADR-015 excluded.** Structural rules answer "is the code shaped @@ -191,6 +197,8 @@ ADR-015 (the structural / registration fitness layer this complements; its state runtime behavior", is exactly this ADR's scope, and the two tiers ship as two separate packages), ADR-014 (the decorator execution order `DecoratorPipelineOrderTestsBase` proves at runtime), ADR-013 (the RFC 9457 ProblemDetails edge contract `ProblemDetailsContractTestsBase` guards), ADR-046 (HTTP -API versioning, whose "shared fitness contract" is one of these bases), ADR-016 (lockstep versioning: +API versioning, whose "shared fitness contract" is one of these bases), ADR-079 (the shared HTTP edge +pipeline, whose own order-checking base ships in this same package but is not one of these six), +ADR-016 (lockstep versioning: a new conformance base reaches consumers only through a release and a full sweep). Package inventory for the framework lives in `MMCA.Common/FACTS.md`. diff --git a/docs-src/adr/059-module-contract-and-composition.md b/docs-src/adr/059-module-contract-and-composition.md index 987a3e0..957df30 100644 --- a/docs-src/adr/059-module-contract-and-composition.md +++ b/docs-src/adr/059-module-contract-and-composition.md @@ -8,7 +8,7 @@ The framework's headline claim is that an application is built as a modular mono extracted into services without rewriting business logic. ADR-008 states the extracted shape in one sentence, "Each service is the monolith with one module enabled", and it treats `ModuleLoader` and the `Disabled*` stubs as **pre-existing context** before deciding the extraction topology -(`Website/docs-src/adr/008-service-extraction-topology.md:29-31`). ADR-006 decides the database axis +(`Website/docs-src/adr/008-service-extraction-topology.md:35-37`). ADR-006 decides the database axis of the same split. Neither records the composition model itself: how a module declares itself, how a host finds it, in what order registrations run, what "disabled" does to the container, and what a dependent resolves when its peer is not in the process. That mechanism is what makes the ADR-008 @@ -40,13 +40,14 @@ rather than by absence. `MMCA.Common/Source/Core/MMCA.Common.Application/Modules/IModuleSeeder.cs:8-19`). An assembly that throws from `GetTypes()` is logged and skipped, not fatal (`ModuleLoader.cs:89-98,353-354`). An overload taking the assemblies explicitly exists and its own documentation calls it the preferred - host call (`ModuleLoader.cs:61-79`), but **every host today uses the AppDomain overload**: Store - (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:245`, - `MMCA.Store.Identity.Service/Program.cs:223`, `MMCA.Store.Sales.Service/Program.cs:229`), ADC + host call (`ModuleLoader.cs:61-79`), but **every host in the three application repos uses the + AppDomain overload today**: Store + (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:247`, + `MMCA.Store.Identity.Service/Program.cs:223`, `MMCA.Store.Sales.Service/Program.cs:231`), ADC (`MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:264`, - `MMCA.ADC.Conference.Service/Program.cs:332`, `MMCA.ADC.Engagement.Service/Program.cs:232`, - `MMCA.ADC.Notification.Service/Program.cs:207`) and Helpdesk - (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:94`). Only the unit tests pass assemblies + `MMCA.ADC.Conference.Service/Program.cs:339`, `MMCA.ADC.Engagement.Service/Program.cs:238`, + `MMCA.ADC.Notification.Service/Program.cs:213`) and Helpdesk + (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:100`). Only the unit tests pass assemblies explicitly (`MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Modules/ModuleLoaderTests.cs:41-48`). - **Registration order is a Kahn topological sort over the declared names.** The loader sorts before it registers anything (`ModuleLoader.cs:112`); the sort builds an in-degree map plus a reverse @@ -154,14 +155,14 @@ Catalog enables `Catalog` and disables both peers with no remote declarations `Sales` and declares `["Catalog", "Identity"]` as `RemoteDependencies` (`MMCA.Store/Source/Services/MMCA.Store.Sales.Service/appsettings.json:27-34`), then replaces the two stubs with typed gRPC clients after the loader returns -(`MMCA.Store/Source/Services/MMCA.Store.Sales.Service/Program.cs:238-239`). ADC Engagement declares +(`MMCA.Store/Source/Services/MMCA.Store.Sales.Service/Program.cs:240-241`). ADC Engagement declares `["Conference"]` remote (`MMCA.ADC/Source/Services/MMCA.ADC.Engagement.Service/appsettings.json:38-46`) and ADC Notification declares `["Identity"]` remote (`MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/appsettings.json:27-35`), while ADC Conference enables one module and declares nothing remote (`MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/appsettings.json:20-25`) even though it wires Engagement's `IBookmarkCountService` as a gRPC client -(`MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:343`), because +(`MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:350`), because `ConferenceModule` never declares Engagement in `Dependencies`. ## Rationale @@ -176,7 +177,7 @@ Engagement's `IBookmarkCountService` as a gRPC client puts its contract type in the container, the dependent module's Application and Domain code has no branch for "peer not present", which is precisely why a module's non-hosting layers are identical in-process and extracted (`ModuleLoader.cs:123`, `CatalogModule.cs:24-25`). The host then - overwrites the stub with a real cross-process adapter (`Sales.Service/Program.cs:238-239`). + overwrites the stub with a real cross-process adapter (`Sales.Service/Program.cs:240-241`). - **Two strictness levels, chosen per module.** A module that genuinely cannot function without a peer opts into `RequiresDependencies = true` and fails fast (`SalesModule.cs:32`, `EngagementModule.cs:23`, `NotificationModule.cs:24`); everything else tolerates a missing peer and @@ -211,10 +212,10 @@ Engagement's `IBookmarkCountService` as a gRPC client so a data-subject export assembled while a module is disabled reads as a complete document. A wrongly disabled module therefore produces plausible wrong answers rather than an error, and the only startup signal is a Degraded `module-{Name}` health check plus a log line - (`DependencyInjection.cs:191-198`, `ModuleLoader.cs:338-339,347-348`). + (`DependencyInjection.cs:200-207`, `ModuleLoader.cs:338-339,347-348`). - **The dependency graph is a hand-written declaration, not a derived fact.** ADC Conference consumes Engagement's `IBookmarkCountService` over gRPC without listing Engagement in `Dependencies` - (`ConferenceModule.cs:15-30` versus `Conference.Service/Program.cs:329`), so neither the + (`ConferenceModule.cs:15-30` versus `Conference.Service/Program.cs:350`), so neither the topological sort nor the `RequiresDependencies` check knows about that edge. Nothing derives `Dependencies` from the interfaces a module actually resolves. What is pinned is the declaration against a written expectation: `ModuleConformanceTestsBase` asserts `Name`, `Dependencies`, diff --git a/docs-src/adr/060-performance-regression-gate.md b/docs-src/adr/060-performance-regression-gate.md index cd3225f..d359018 100644 --- a/docs-src/adr/060-performance-regression-gate.md +++ b/docs-src/adr/060-performance-regression-gate.md @@ -96,9 +96,11 @@ and Shared and sits deliberately outside `MMCA.Common.slnx` so the unit-test loo **This gate is MMCA.Common only.** The harness, the baseline and the verifier exist in that repo and nowhere else. MMCA.ADC and MMCA.Store have no benchmark suite and no perfgate; their performance -artifact is a k6 load test against deployed read endpoints, which runs monthly on a schedule and on -demand, not on a pull request (`MMCA.ADC/.github/workflows/load-test.yml:1-18`, -`MMCA.Store/.github/workflows/load-test.yml:8-18`). MMCA.Helpdesk has neither. +artifact for backend hot paths is a k6 load test against deployed read endpoints, which runs monthly +on a schedule and on demand, not on a pull request (`MMCA.ADC/.github/workflows/load-test.yml:1-18`, +`MMCA.Store/.github/workflows/load-test.yml:8-18`). MMCA.Helpdesk has neither. The client-side +counterpart, a Core Web Vitals budget asserted per deploy inside the chromium e2e-gate, is +[ADR-092](092-web-vitals-budget-gate.md)'s decision, not this gate's. ## Rationale - **A ratio is a property of the code; an absolute nanosecond count is a property of the runner.** diff --git a/docs-src/adr/061-runtime-secret-management.md b/docs-src/adr/061-runtime-secret-management.md index 411fcad..60c3738 100644 --- a/docs-src/adr/061-runtime-secret-management.md +++ b/docs-src/adr/061-runtime-secret-management.md @@ -1,7 +1,7 @@ # ADR-061: Runtime Secret Management via Key Vault References and Managed Identity ## Status -Accepted (2026-08-01; citations re-anchored 2026-08-14). +Accepted (2026-08-01; vault-backed configuration source recorded and citations re-anchored 2026-08-23). ## Context A Container App can hold a credential two ways: as a literal value in the app's own `secrets` @@ -14,7 +14,7 @@ secrets and an Anthropic key in ADC; Stripe secret and webhook keys in Store). The literal form turns the deployment template into the distribution mechanism for every one of those values and leaves a readable copy in each app's own configuration, where rotation means editing every app that uses the value. Nothing in the record decided which form to use. ADR-037 tells a consumer to -keep its field-encryption key in Key Vault (`037-field-level-encryption-at-rest.md:79`) without +keep its field-encryption key in Key Vault (`037-field-level-encryption-at-rest.md:108-110`) without deciding how a running app reaches it; ADR-045 notes that blob storage authenticates with `DefaultAzureCredential` and needs a data-plane role rather than a secret (`045-managed-file-storage-and-avatars.md:23`, `:50`); ADR-053 decides publish-time identity. None of @@ -23,55 +23,78 @@ migration that is removing the last password from that set. ## Decision Every production secret lives in Azure Key Vault and reaches the app as a `keyVaultUrl` secret -reference resolved by a user-assigned managed identity. SQL authentication is staged behind a flag on +reference resolved by a user-assigned managed identity; the same identity also lets a host read the +vault directly as a configuration source at startup. SQL authentication is staged behind a flag on its way to the same model. - **The apps run as one shared user-assigned managed identity, referenced as `existing`.** - `adc-prod-apps-identity` (`MMCA.ADC/infra/main.bicep:920-922`) and `mmca-prod-apps-identity` - (`MMCA.Store/infra/main.bicep:761-763`). Every container app attaches it - (`MMCA.ADC/infra/main.bicep:1022-1027`, `:1225`, `:1349`, `:1473`, `:1634`, `:1736`; - `MMCA.Store/infra/main.bicep:943-948`, `:1097`, `:1196`, `:1327`, `:1412`), and the same identity is + `adc-prod-apps-identity` (`MMCA.ADC/infra/main.bicep:931-933`) and `mmca-prod-apps-identity` + (`MMCA.Store/infra/main.bicep:772-774`). Every container app attaches it + (`MMCA.ADC/infra/main.bicep:1036`, `:1243`, `:1372`, `:1501`, `:1667`, `:1770`; + `MMCA.Store/infra/main.bicep:957`, `:1116`, `:1221`, `:1358`, `:1444`), and the same identity is the ACR pull credential, so no registry admin password exists either - (`MMCA.ADC/infra/main.bicep:925-928`, `MMCA.Store/infra/main.bicep:766-769`). + (`MMCA.ADC/infra/main.bicep:936-939`, `MMCA.Store/infra/main.bicep:777-780`). - **The vault is referenced, not created; the deployment writes the values into it.** The template - declares the vault as `existing` (`MMCA.ADC/infra/main.bicep:940-942`, - `MMCA.Store/infra/main.bicep:849-851`) and then writes secret child resources: fourteen in ADC - (`MMCA.ADC/infra/main.bicep:944-1013`) and eleven in Store - (`MMCA.Store/infra/main.bicep:853-907`). + declares the vault as `existing` (`MMCA.ADC/infra/main.bicep:951-953`, + `MMCA.Store/infra/main.bicep:860-862`) and then writes secret child resources: fourteen in ADC + (`MMCA.ADC/infra/main.bicep:955-1024`) and eleven in Store + (`MMCA.Store/infra/main.bicep:864-918`). - **Every Container App secret entry is a `keyVaultUrl` reference bound to that identity.** ADC - Identity (`MMCA.ADC/infra/main.bicep:1041-1056`), Conference (`:1242-1249`), Engagement - (`:1367-1369`), Notification (`:1500-1508`); Store Identity - (`MMCA.Store/infra/main.bicep:962-975`), Catalog (`:1114-1118`), Sales (`:1225-1236`). Not one entry + Identity (`MMCA.ADC/infra/main.bicep:1052-1067`), Conference (`:1259-1266`), Engagement + (`:1388-1392`), Notification (`:1527-1535`); Store Identity + (`MMCA.Store/infra/main.bicep:973-986`), Catalog (`:1132-1136`), Sales (`:1249-1260`). Not one entry carries an inline `value`. The two apps that need no credential say so explicitly: the Gateway and UI apps declare `secrets: []` - (`MMCA.ADC/infra/main.bicep:1649`, `:1754`; `MMCA.Store/infra/main.bicep:1342`, `:1430`). + (`MMCA.ADC/infra/main.bicep:1681`, `:1787`; `MMCA.Store/infra/main.bicep:1372`, `:1461`). - **Containers consume secrets only through `secretRef`.** The SQL connection string - (`MMCA.ADC/infra/main.bicep:1077`, `MMCA.Store/infra/main.bicep:1256`), the broker connection string - (`MMCA.ADC/infra/main.bicep:1556`, `MMCA.Store/infra/main.bicep:1276`), the RSA private key and its - HS256 fallback (`MMCA.ADC/infra/main.bicep:1153`, `:1160`; `MMCA.Store/infra/main.bicep:1049`, - `:1056`), SMTP (`MMCA.ADC/infra/main.bicep:1163`, `:1576`; `MMCA.Store/infra/main.bicep:1059`, - `:1298`), the OAuth client secrets (`MMCA.ADC/infra/main.bicep:1166`, `:1170`), the Anthropic key - (`:1301`), the native-push hub connection string (`:1574`), and the two Stripe keys - (`MMCA.Store/infra/main.bicep:1294-1295`). + (`MMCA.ADC/infra/main.bicep:1089`, `MMCA.Store/infra/main.bicep:1281`), the broker connection string + (`MMCA.ADC/infra/main.bicep:1588`, `MMCA.Store/infra/main.bicep:1301`), the RSA private key and its + HS256 fallback (`MMCA.ADC/infra/main.bicep:1170`, `:1177`; `MMCA.Store/infra/main.bicep:1067`, + `:1074`), SMTP (`MMCA.ADC/infra/main.bicep:1180`, `:1608`; `MMCA.Store/infra/main.bicep:1077`, + `:1328`), the OAuth client secrets (`MMCA.ADC/infra/main.bicep:1183`, `:1187`), the Anthropic key + (`:1323`), the native-push hub connection string (`:1606`), and the two Stripe keys + (`MMCA.Store/infra/main.bicep:1324-1325`). +- **A second, host-side path reads the same vault as a configuration source.** Alongside the + platform-resolved references, both templates set `KeyVault__Uri` and `AZURE_CLIENT_ID` on every app + whose host calls `AddCommonKeyVaultConfiguration` + (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Configuration/KeyVaultConfigurationExtensions.cs:78-112`), + which layers the vault over `IConfiguration` at startup. Store wires all five deployables through + two shared env entries (`MMCA.Store/infra/main.bicep:932-935`, `:942-945`, used at `:1006-1007`, + `:1154-1155`, `:1279-1280`, `:1392-1393`, `:1497-1498`); ADC wires five of its six apps, the four + services and the UI but not the Gateway (`MMCA.ADC/infra/main.bicep:1148`, `:1156`; `:1320-1321`; + `:1451-1452`; `:1597-1598`; `:1816`, `:1819`, documented at `:948-950`). The calls themselves sit + in each host's `Program.cs`: Store `Identity.Service:48`, `Catalog.Service:41`, `Sales.Service:60`, + `Gateway:50`, `UI.Web:41`; ADC `Identity.Service:119`, `Conference.Service:124`, + `Engagement.Service:106`, `Notification.Service:109`, `UI.Web:38`. The call is + gated on `KeyVault:Uri` and does nothing at all without it, so local runs and tests take no Azure + dependency (`KeyVaultConfigurationExtensions.cs:80-88`). It authenticates with + `DefaultAzureCredential` against the same Key Vault Secrets User grant the references already use, + which is what makes `AZURE_CLIENT_ID` load-bearing: these apps carry only a user-assigned identity, + so an unpinned client id fails the startup read (`MMCA.Store/infra/main.bicep:937-941`, + `MMCA.ADC/infra/main.bicep:1149-1155`). Secret names map `--` onto the configuration separator + (`KeyVaultConfigurationExtensions.cs:43-48`, `:109`), and no secret in either vault carries `--` + today, so this adds a source without re-pointing any setting the apps already bind + (`MMCA.Store/infra/main.bicep:925-928`). - **Composite connection strings are assembled at deploy time and land only in the vault.** The Redis - string embeds a key read with `listKeys()` (`MMCA.ADC/infra/main.bicep:895`, - `MMCA.Store/infra/main.bicep:736`), the broker string comes from a dedicated `app-clients` SAS rule + string embeds a key read with `listKeys()` (`MMCA.ADC/infra/main.bicep:906`, + `MMCA.Store/infra/main.bicep:747`), the broker string comes from a dedicated `app-clients` SAS rule rather than the namespace root (`MMCA.ADC/infra/main.bicep:161-164`, `MMCA.Store/infra/main.bicep:135-138`), and the per-database SQL strings are composed from a shared base (`MMCA.ADC/infra/main.bicep:152-159`, `MMCA.Store/infra/main.bicep:127-133`). All of them are written straight into vault secrets, so the assembled value never appears in app configuration. - **An unconfigured optional secret gets a placeholder, not a missing entry.** Optional values are - written as the literal `unused` when their parameter is empty (`MMCA.ADC/infra/main.bicep:982`, - `:987`, `:992`, `:997`, `:1002`, `:1007`, `:1012`; `MMCA.Store/infra/main.bicep:881`, `:886`, `:891`, - `:896`, `:901`, `:906`), while the app-side reference is conditional (for example `hasSmtpPassword` - at `MMCA.ADC/infra/main.bicep:1053` and `hasStripe` at `MMCA.Store/infra/main.bicep:1231-1234`), so - the vault entry always exists but an unconfigured feature is simply absent from the app. + written as the literal `unused` when their parameter is empty (`MMCA.ADC/infra/main.bicep:993`, + `:998`, `:1003`, `:1008`, `:1013`, `:1018`, `:1023`; `MMCA.Store/infra/main.bicep:892`, `:897`, + `:902`, `:907`, `:912`, `:917`), while the app-side reference is conditional (for example + `hasSmtpPassword` at `MMCA.ADC/infra/main.bicep:1064` and `hasStripe` at + `MMCA.Store/infra/main.bicep:1255-1258`), so the vault entry always exists but an unconfigured + feature is simply absent from the app. - **The two role assignments are bootstrapped out of band, deliberately.** The deploy identity holds Key Vault Secrets Officer to write the values; the apps hold Key Vault Secrets User to read them; the vault and both grants are created outside the template because the deploy principal has - Contributor without role-assignment-write (`MMCA.ADC/infra/main.bicep:933-936`, - `MMCA.Store/infra/main.bicep:845-848`). It is the same least-privilege posture that keeps ADR-045's - avatar-storage grant behind a default-false flag (`MMCA.ADC/infra/main.bicep:112-113`, `:840-848`). + Contributor without role-assignment-write (`MMCA.ADC/infra/main.bicep:944-950`, + `MMCA.Store/infra/main.bicep:856-859`). It is the same least-privilege posture that keeps ADR-045's + avatar-storage grant behind a default-false flag (`MMCA.ADC/infra/main.bicep:112-113`, `:851-859`). The bootstrap commands are written out in the framework's reference runbook (`MMCA.Common/samples/deployment/DEPLOYMENT.md:14-35`). - **SQL authentication is staged behind `useManagedIdentitySql`, and the stage is additive.** The @@ -81,13 +104,15 @@ its way to the same model. plus password (`MMCA.ADC/infra/main.bicep:152-154`, `MMCA.Store/infra/main.bicep:127-129`). The Entra admin the flip depends on is provisioned only when its object id is supplied and does not set `azureADOnlyAuthentication`, so password login keeps working during the transition - (`MMCA.ADC/infra/main.bicep:606-621`, `MMCA.Store/infra/main.bicep:554-569`). The pipeline exposes + (`MMCA.ADC/infra/main.bicep:617-632`, `MMCA.Store/infra/main.bicep:565-580`). The pipeline exposes the same three stages: supply the Entra admin, run the per-database external-provider grants by hand, then set the flag (`MMCA.ADC/.github/workflows/deploy.yml:1050-1068`, - `MMCA.Store/.github/workflows/deploy.yml:1014-1032`), driven by repository variables that are + `MMCA.Store/.github/workflows/deploy.yml:1015-1033`), driven by repository variables that are absent by default (`MMCA.ADC/.github/workflows/deploy.yml:930-932`). -**Adoption boundary.** The Key Vault half is shipped and identical in both deployed apps; the SQL half +**Adoption boundary.** The secret-reference half is shipped and identical in both deployed apps, and +the configuration-source half is shipped in both with one difference: Store wires its Gateway +(`MMCA.Store/infra/main.bicep:1392-1393`) while ADC's Gateway carries neither variable. The SQL half is staged in both and not flipped in either template default, so with the default parameters every app-to-database connection string still carries `User ID` and `Password` (`MMCA.ADC/infra/main.bicep:154`, `MMCA.Store/infra/main.bicep:129`). That password is itself a vault @@ -108,14 +133,14 @@ consumer-by-consumer state is named. ## Rationale - **A reference has one home; a literal has as many homes as it has consumers.** Three vault secrets in each repo are referenced by more than one app: Redis and the broker by all four ADC services - (`MMCA.ADC/infra/main.bicep:1044-1045`, `:1245-1246`, `:1368-1369`, `:1503-1504`) and all three - Store services (`MMCA.Store/infra/main.bicep:965-966`, `:1116-1117`, `:1228-1229`), and the SMTP - password by two apps in each (`MMCA.ADC/infra/main.bicep:1053`, `:1507`; - `MMCA.Store/infra/main.bicep:974`, `:1235`). As references they are one vault entry pointed at from + (`MMCA.ADC/infra/main.bicep:1055-1056`, `:1262-1263`, `:1390-1391`, `:1530-1531`) and all three + Store services (`MMCA.Store/infra/main.bicep:976-977`, `:1134-1135`, `:1252-1253`), and the SMTP + password by two apps in each (`MMCA.ADC/infra/main.bicep:1064`, `:1534`; + `MMCA.Store/infra/main.bicep:985`, `:1259`). As references they are one vault entry pointed at from several apps; as literals they would be several copies to keep in step. - **Reuse the identity that already existed.** The user-assigned identity was introduced to pull images from ACR without the registry admin password - (`MMCA.ADC/infra/main.bicep:915-919`, `MMCA.Store/infra/main.bicep:756-760`). Granting it Key Vault + (`MMCA.ADC/infra/main.bicep:926-930`, `MMCA.Store/infra/main.bicep:767-771`). Granting it Key Vault Secrets User extends one principal rather than introducing a second credential-holding model, and leaves one thing to audit. - **Keeping the grants out of the template is what keeps the deploy identity least-privileged.** A @@ -133,7 +158,7 @@ consumer-by-consumer state is named. several more out-of-band bootstraps. - **The template alone does not stand up an environment.** The vault, the identity, its AcrPull grant and both Key Vault roles must already exist; `main.bicep` references them - (`MMCA.ADC/infra/main.bicep:920-922`, `:940-942`) and cannot report that a grant is missing. The + (`MMCA.ADC/infra/main.bicep:931-933`, `:951-953`) and cannot report that a grant is missing. The prerequisites live in each repo's private `infra/DISASTER-RECOVERY.md` and, in distilled form, in `MMCA.Common/samples/deployment/DEPLOYMENT.md:14-35`. - **The pipeline is still a plaintext path.** Values arrive as `@secure()` bicep parameters written @@ -146,16 +171,24 @@ consumer-by-consumer state is named. - **Two literal values remain in the template.** The Application Insights connection string is an ordinary env var (`MMCA.ADC/infra/main.bicep:200-203`, `MMCA.Store/infra/main.bicep:174-177`) and the Log Analytics shared key is passed inline to the managed environment - (`MMCA.ADC/infra/main.bicep:905-911`, `MMCA.Store/infra/main.bicep:746-752`). Both are telemetry + (`MMCA.ADC/infra/main.bicep:911-922`, `MMCA.Store/infra/main.bicep:752-765`). Both are telemetry ingestion keys read with `listKeys()` at deploy time, not application credentials, and neither is covered by this decision. +- **The host-side vault read is a hard startup dependency.** The configuration source is added + synchronously in the host builder, so a vault read that cannot authenticate (an unpinned + `AZURE_CLIENT_ID` is the documented case) crash-loops the app rather than degrading one feature + (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Configuration/KeyVaultConfigurationExtensions.cs:63-69`, + `MMCA.Store/infra/main.bicep:938-941`). Neither template sets `KeyVault:ReloadIntervalMinutes`, so + the vault is read once at startup and a rotated secret reaches those hosts on their next restart + (`KeyVaultConfigurationExtensions.cs:37-39`, `:96-107`). - **The staged SQL half is inert until an operator acts.** Until the per-database `CREATE USER ... FROM EXTERNAL PROVIDER` grants are run and the flag is set, the shared SQL admin login is still what every service authenticates with, so password rotation is deferred rather than solved. Same audit-the-inventory caveat as ADR-018 and ADR-020. ## Related -ADR-037 (directs a consumer to keep the field-encryption key in Key Vault but decides no delivery +ADR-037 (`037-field-level-encryption-at-rest.md:108-110` directs a consumer to keep the +field-encryption key in Key Vault but decides no delivery mechanism, and nothing wires that converter today, so no such secret exists in either vault), ADR-045 (the identity model one layer out: blob access is a data-plane role on the same identity instead of a secret, which is why its grant carries the same out-of-band caveat), ADR-053 (the diff --git a/docs-src/adr/062-slo-alerting-as-code.md b/docs-src/adr/062-slo-alerting-as-code.md index 03fa371..e806139 100644 --- a/docs-src/adr/062-slo-alerting-as-code.md +++ b/docs-src/adr/062-slo-alerting-as-code.md @@ -4,6 +4,11 @@ Accepted (2026-08-01). Revised 2026-08-18: Store's two operational extras (the `outbox-dead-letter` scheduled query rule and the outside-in Gateway availability web test with its severity 1 metric alert) merged to `main` on 2026-08-13 and are recorded here as shipped rather than as in flight. +Revised 2026-08-23: both templates grew above the alert block, so every `main.bicep` citation is +re-anchored; two statements are corrected. Store's `OPERATIONS.md` does carry triage for its two +operational extras (under `####` headings, deliberately outside the gate's `###` match), and Store's +resource prefix is `mmca-`, not `store-`, which makes the heading-versus-deployed-name trade-off +wider than previously recorded. No decision changed. ## Context ADR-041 standardized what the fleet **emits**: RED histograms off the CQRS pipeline, an outbox @@ -28,7 +33,7 @@ Three forces shaped the decision. 2026-07-29 resolved to exactly that, one window holding 8x401 plus 2x499 plus a single readiness 503 and zero other failures, and five hub connections averaging 11.3s dragged the fleet-wide average to 5539ms against a 3000ms threshold while every real request was fast - (`MMCA.ADC/infra/main.bicep:247-260`). + (`MMCA.ADC/infra/main.bicep:273-286`). ## Decision Declare each consumer's SLO alerts as **data in its Bicep template**, materialize them as Log Analytics @@ -36,52 +41,52 @@ scheduled query rules, and make the alert-to-runbook pairing a **build gate ship - **`sloAlertSpecs` is the declaration.** A single array of records carrying `key`, `description`, `query`, `timeAggregation`, `metricMeasureColumn`, `threshold` and `severity` - (`MMCA.ADC/infra/main.bicep:276`, `MMCA.Store/infra/main.bicep:248`). Both consumers declare the same + (`MMCA.ADC/infra/main.bicep:287`, `MMCA.Store/infra/main.bicep:259`). Both consumers declare the same three SLOs with the same numbers: `failed-requests` (severity 2, more than 10 per 15 min), `server-response-time` (severity 3, average above 3000ms), `dependency-failures` (severity 2, more - than 10 per 15 min) (`MMCA.ADC/infra/main.bicep:278-303`, `MMCA.Store/infra/main.bicep:250-275`). + than 10 per 15 min) (`MMCA.ADC/infra/main.bicep:288-315`, `MMCA.Store/infra/main.bicep:260-287`). - **Materialized as Log Analytics scheduled query rules.** One `Microsoft.Insights/scheduledQueryRules` - per spec (`MMCA.ADC/infra/main.bicep:306`, `MMCA.Store/infra/main.bicep:278`), named - `${prefix}-alert-${spec.key}-v2` (`MMCA.ADC/infra/main.bicep:311`, - `MMCA.Store/infra/main.bicep:283`), enabled, scoped to the Log Analytics workspace, evaluated every + per spec (`MMCA.ADC/infra/main.bicep:317`, `MMCA.Store/infra/main.bicep:289`), named + `${prefix}-alert-${spec.key}-v2` (`MMCA.ADC/infra/main.bicep:322`, + `MMCA.Store/infra/main.bicep:294`), enabled, scoped to the Log Analytics workspace, evaluated every 5 minutes over a 15-minute window with `autoMitigate` - (`MMCA.ADC/infra/main.bicep:318-322`, `MMCA.Store/infra/main.bicep:290-294`). A `union(...)` supplies + (`MMCA.ADC/infra/main.bicep:329-333`, `MMCA.Store/infra/main.bicep:301-305`). A `union(...)` supplies `metricMeasureColumn` only for the aggregate rule; the empty-string case makes a rule count returned **rows**, which is what the two failure-count SLOs want - (`MMCA.ADC/infra/main.bicep:328-341`, `MMCA.Store/infra/main.bicep:300-313`). + (`MMCA.ADC/infra/main.bicep:339-351`, `MMCA.Store/infra/main.bicep:311-323`). - **The KQL predicate is the point of the migration.** The failure queries exclude only 401 and 499 - (`MMCA.ADC/infra/main.bicep:280`, `:298`; `MMCA.Store/infra/main.bicep:252`, `:270`) and the latency + (`MMCA.ADC/infra/main.bicep:291`, `:309`; `MMCA.Store/infra/main.bicep:263`, `:281`) and the latency query excludes `/hubs/` requests before averaging `DurationMs` - (`MMCA.ADC/infra/main.bicep:289`, `MMCA.Store/infra/main.bicep:261`). A genuine 400 or 500 burst + (`MMCA.ADC/infra/main.bicep:300`, `MMCA.Store/infra/main.bicep:272`). A genuine 400 or 500 burst still pages at the same threshold as before. - **Superseded metric alerts stay declared and disabled, never deleted.** `legacySloMetricAlertSpecs` - (`MMCA.ADC/infra/main.bicep:357`, `MMCA.Store/infra/main.bicep:328`) still materializes the three - `metricAlerts` (`MMCA.ADC/infra/main.bicep:363`, `MMCA.Store/infra/main.bicep:334`) under their - **original, unsuffixed** names (`MMCA.ADC/infra/main.bicep:365`, `MMCA.Store/infra/main.bicep:336`) - with `enabled: false` and an empty `actions` array (`MMCA.ADC/infra/main.bicep:370`, `:389`; - `MMCA.Store/infra/main.bicep:341`, `:360`). An incremental ARM deployment never deletes a resource + (`MMCA.ADC/infra/main.bicep:368`, `MMCA.Store/infra/main.bicep:339`) still materializes the three + `metricAlerts` (`MMCA.ADC/infra/main.bicep:374`, `MMCA.Store/infra/main.bicep:345`) under their + **original, unsuffixed** names (`MMCA.ADC/infra/main.bicep:376`, `MMCA.Store/infra/main.bicep:347`) + with `enabled: false` and an empty `actions` array (`MMCA.ADC/infra/main.bicep:381`, `:400`; + `MMCA.Store/infra/main.bicep:352`, `:371`). An incremental ARM deployment never deletes a resource just because it left the template, so dropping them would have left them live and firing alongside the new rules; disabling them in place is declarative, needs no portal step, and rolls back in one line. That is also why the replacements carry the `-v2` suffix: reusing the name would have renamed - the live originals instead of disabling them (`MMCA.ADC/infra/main.bicep:308-310`, - `MMCA.Store/infra/main.bicep:280-282`). + the live originals instead of disabling them (`MMCA.ADC/infra/main.bicep:319-321`, + `MMCA.Store/infra/main.bicep:291-293`). - **One unconditional action group.** `alertEmailAddress` is a required parameter with no default (`MMCA.ADC/infra/main.bicep:104`, `MMCA.Store/infra/main.bicep:87`), so the action group's email - receiver is not conditional (`MMCA.ADC/infra/main.bicep:246-260`, - `MMCA.Store/infra/main.bicep:220-234`) and every scheduled query rule routes to it - (`MMCA.ADC/infra/main.bicep:344`, `MMCA.Store/infra/main.bicep:316`). The monthly cost budget - notifies the same group (`MMCA.ADC/infra/main.bicep:551`, `:560`; - `MMCA.Store/infra/main.bicep:508`, `:517`). + receiver is not conditional (`MMCA.ADC/infra/main.bicep:257-271`, + `MMCA.Store/infra/main.bicep:231-245`) and every scheduled query rule routes to it + (`MMCA.ADC/infra/main.bicep:354`, `MMCA.Store/infra/main.bicep:326`). The monthly cost budget + notifies the same group (`MMCA.ADC/infra/main.bicep:578`, `:586`; + `MMCA.Store/infra/main.bicep:535`, `:543`). - **A saved workbook renders the same three signals.** `sloWorkbook` - (`MMCA.ADC/infra/main.bicep:530`, `MMCA.Store/infra/main.bicep:487`) is bound to the Log Analytics + (`MMCA.ADC/infra/main.bicep:541`, `MMCA.Store/infra/main.bicep:498`) is bound to the Log Analytics workspace and embeds `workbooks/adc-slo-workbook.json` / `workbooks/store-slo-workbook.json` at - compile time via `loadTextContent` (`MMCA.ADC/infra/main.bicep:539`, - `MMCA.Store/infra/main.bicep:496`), grouped per service by `AppRoleName`, so the visualization cannot + compile time via `loadTextContent` (`MMCA.ADC/infra/main.bicep:550`, + `MMCA.Store/infra/main.bicep:507`), grouped per service by `AppRoleName`, so the visualization cannot diverge from the alerts by being maintained somewhere else. - **`infra/OPERATIONS.md` is the paired artifact.** Each repo's runbook carries one `###` section per @@ -137,20 +142,29 @@ deployment. **Coverage boundary inside ADC's template.** The gate covers exactly the alerts declared between the two parse anchors. ADC additionally provisions two operational scheduled query rules, -`outbox-dead-letter` and `sql-dependency-failures` (`MMCA.ADC/infra/main.bicep:407`, materialized at -`:422`), and a severity 1 gateway-availability metric alert over a three-location URL ping web test -(`MMCA.ADC/infra/main.bicep:463`, `:496`). All three sit outside the `sloAlertSpecs` window, so the +`outbox-dead-letter` and `sql-dependency-failures` (`MMCA.ADC/infra/main.bicep:418`, materialized at +`:433`), and a severity 1 gateway-availability metric alert over a three-location URL ping web test +(`MMCA.ADC/infra/main.bicep:474`, `:507`). All three sit outside the `sloAlertSpecs` window, so the pairing gate neither requires nor forbids runbook sections for them, and `OPERATIONS.md` carries none today (its only `-alert-` headings are the three SLO sections). Store provisions both of the families that apply to it, merged on 2026-08-13: the `outbox-dead-letter` scheduled query rule -(`MMCA.Store/infra/main.bicep:379`) and the outside-in Gateway availability web test (`:420`) with its -severity 1 metric alert (`:453`, severity at `:459`), alongside the three SLO rules and the budget -notifications. Both sit after `resource sloAlerts` (`MMCA.Store/infra/main.bicep:334`) and therefore -outside the parse window, so Store's two extras are ungated exactly as ADC's three are, and Store's -`OPERATIONS.md` likewise carries only the three SLO sections (`MMCA.Store/infra/OPERATIONS.md:16`, -`:31`, `:44`). It deliberately -does not port `sql-dependency-failures`: Store's own `dependency-failures` SLO rule already spans -SQL, gRPC and HTTP, so a narrower SQL-scoped twin would page twice for one fault. +(`MMCA.Store/infra/main.bicep:390`) and the outside-in Gateway availability web test (`:431`) with its +severity 1 metric alert (`:464`, severity at `:470`), alongside the three SLO rules and the budget +notifications. Both sit after `resource sloAlerts` (`MMCA.Store/infra/main.bicep:345`) and therefore +outside the parse window, so Store's two extras are ungated exactly as ADC's three are. + +The two consumers then diverge in how they treat that ungated space. ADC writes no triage for its +three; Store does write it, and keeps it out of the gate's reach on purpose. Store's `OPERATIONS.md` +carries an `## Operational alert runbooks` section (`MMCA.Store/infra/OPERATIONS.md:58`) holding +`store-alert-outbox-dead-letter` (sev 2) at `:66` and `store-alert-gateway-availability` (sev 1) at +`:89`. Both are `####` headings rather than `###`, because `RunbookHeadingRegex` is `^###\s+.*$` +(`ObservabilityConventionTestsBase.cs:145-146`) and does not match a `####` line: an `###` heading +naming a non-spec alert would read to `EveryRunbookAlertSection_MapsToAProvisionedAlert` as an orphan +section and fail the build. The runbook states that reasoning inline +(`MMCA.Store/infra/OPERATIONS.md:62-64`). So the triage exists and is discoverable at 3am, while the +gate still sees exactly three paired alerts on each side. Store deliberately does not port +`sql-dependency-failures`: its own `dependency-failures` SLO rule already spans SQL, gRPC and HTTP +(`MMCA.Store/infra/main.bicep:388-389`), so a narrower SQL-scoped twin would page twice for one fault. ## Rationale - **Alerts as data, not as portal state.** One array is reviewable in a PR, diffable across @@ -186,16 +200,24 @@ SQL, gRPC and HTTP, so a narrower SQL-scoped twin would page twice for one fault all remain review concerns. Severity is the only value cross-checked between the two files. - **Only the spec-window alerts are covered.** ADC's outbox dead-letter, SQL dependency and gateway-availability alerts and Store's outbox dead-letter and gateway-availability alerts are - provisioned but ungated, so all five can be added, renamed or re-tiered with no runbook - consequence. + provisioned but ungated, so all five can be added, renamed or re-tiered with no **build** + consequence. That is not the same as no consequence: Store's two do have written triage + (`MMCA.Store/infra/OPERATIONS.md:66`, `:89`), and because those `####` sections are invisible to + the gate by design, re-tiering or renaming either alert leaves stale runbook text that nothing + checks. The honour-system caveat is recorded in the runbook itself + (`MMCA.Store/infra/OPERATIONS.md:126-128`). - **Disabled-but-declared alerts are carried debt.** Three superseded `metricAlerts` remain in each template and in each resource group until a follow-up deletes them, and the templates say so - (`MMCA.ADC/infra/main.bicep:350-356`, `MMCA.Store/infra/main.bicep:322-327`). + (`MMCA.ADC/infra/main.bicep:361-367`, `MMCA.Store/infra/main.bicep:333-338`). - **The runbook heading is not the deployed resource name.** The gate matches only the `-alert-` - infix, so the prefix in a heading is unchecked. Live rules resolve from `prefix` - (`MMCA.ADC/infra/main.bicep:125`, `MMCA.Store/infra/main.bicep:99`) and carry the `-v2` suffix, while - the headings read `adc-alert-failed-requests` and `store-alert-failed-requests`: close enough to - search for, not a literal copy of what an operator sees in the portal. + infix, so the prefix in a heading is unchecked. Live rules resolve from `prefix` and carry the `-v2` + suffix, while the headings read `adc-alert-failed-requests` and `store-alert-failed-requests`. On ADC + the gap is only the environment segment and the suffix (`prefix` is `adc-${environmentName}`, + `MMCA.ADC/infra/main.bicep:125`). On Store the prefix does not match at all: `prefix` is + `mmca-${environmentName}` (`MMCA.Store/infra/main.bicep:105`), so the deployed rule is + `mmca--alert-failed-requests-v2` against a heading that starts `store-`. A heading is therefore + a searchable handle, not a literal copy of what an operator sees in the portal, and on Store it is + not even a prefix match on the resource name. - **One action group, one receiver, no routing.** Severity 1 and severity 3 land in the same inbox, alongside budget notifications. Severity is metadata for triage order, not a delivery decision. - **Adoption is opt-in per consumer.** A consumer that provisions alerts without embedding the two diff --git a/docs-src/adr/063-accessibility-conformance-gate.md b/docs-src/adr/063-accessibility-conformance-gate.md index 58ef8c9..6cc6b96 100644 --- a/docs-src/adr/063-accessibility-conformance-gate.md +++ b/docs-src/adr/063-accessibility-conformance-gate.md @@ -3,7 +3,11 @@ ## Status Accepted (2026-08-01). Revised 2026-08-14: refreshed the `E2ETestBase` helper line anchors (explanatory comments were added above `ScanGridAsync`), the two consumer suite scan counts (ADC now 31, Store now 23), -and the MMCA.Helpdesk pin location; no decision changed. +and the MMCA.Helpdesk pin location. Revised 2026-08-23: the forgot-password vertical +([ADR-091](091-cache-backed-password-reset.md)) added a fourth asserting Identity workflow base and three +gallery assertions, so the base count, the gallery count (now eleven assertions across seven classes), and +both consumer subclass counts are updated, along with a correction to ADC's grid/strict split; no decision +changed. ## Context Accessibility was documented before it was enforced. The narrative guide @@ -42,10 +46,13 @@ the package's own workflow bases, and wire it as a required merge check and a de `.../Testing.E2E/Infrastructure/AccessibilityViolationException.cs:7`) carrying each rule's impact, id, help text, and the offending markup compacted to one line per node (`:170-178`), so a red gate points at the element rather than at a dashboard. -- **The package's Identity workflow bases assert it, so a consumer inherits the scan.** A subclass - gets a login-page, register-page, and profile-page a11y test with no test code of its own: +- **The package's Identity workflow bases assert it, so a consumer inherits the scan.** Four bases carry + the assertion, so a subclass gets a login-page, register-page, profile-page, forgot-password-page, and + reset-password-page a11y test with no test code of its own: `.../Testing.E2E/Workflows/Identity/UserLoginTestsBase.cs:83`, - `UserRegistrationTestsBase.cs:91`, and `ProfileManagementTestsBase.cs:180` each call the assert with + `UserRegistrationTestsBase.cs:91`, `ProfileManagementTestsBase.cs:180`, and + `PasswordResetTestsBase.cs` (class at `:17`, the two scans asserting at `:88` and `:99`, added with the + password-recovery flow of [ADR-091](091-cache-backed-password-reset.md)) each call the assert with `AxeOptions.Wcag21Aa`. - **Consumer page scans go through two helpers that make strictness explicit.** On `.../Testing.E2E/Infrastructure/E2ETestBase.cs`, `ScanAsync()` (`:293`) waits for any loading bar to @@ -78,20 +85,26 @@ the package's own workflow bases, and wire it as a required merge check and a de surface ratio (`.../MMCA.Common.UI/Theme/BrandColors.cs:26`, rationale at `:22-24`). Adoption differs per repo and is uneven on purpose. **MMCA.Common** scans its own backend-less gallery: -eight assertions across five classes over `GalleryAxeTestBase` +eleven assertions across seven classes over `GalleryAxeTestBase` (`MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/Infrastructure/GalleryAxeTestBase.cs:14`), covering login (`LoginPageE2ETests.cs:34`), register (`RegisterPageE2ETests.cs:36`), the primitives -showcase (`ComponentsPageE2ETests.cs:80`), both dark-mode states (`DarkModeE2ETests.cs:30,:40`), and the +showcase (`ComponentsPageE2ETests.cs:80`), both dark-mode states (`DarkModeE2ETests.cs:30,:40`), the notification pages, one of which is the gallery's use of the pager exception -(`NotificationPagesE2ETests.cs:32`, alongside strict scans at `:44` and `:56`). **MMCA.Store** subclasses -all three Identity bases (`Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5`, -`UserRegistrationTests.cs:5`, `ProfileManagementTests.cs:5`). **MMCA.ADC** subclasses two +(`NotificationPagesE2ETests.cs:32`, alongside strict scans at `:44` and `:56`), and the password-recovery +pages added with [ADR-091](091-cache-backed-password-reset.md): forgot-password in both its form state and +its confirmation state, which replaces the form and is therefore a distinct rendering +(`ForgotPasswordPageE2ETests.cs:46` and `:60`), plus reset-password (`ResetPasswordPageE2ETests.cs:48`). +**MMCA.Store** subclasses all four Identity bases +(`Tests/E2E/MMCA.Store.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5`, +`UserRegistrationTests.cs:5`, `ProfileManagementTests.cs:5`, `PasswordResetTests.cs:5`). **MMCA.ADC** +subclasses three of the four (`Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/Identity/UserLoginTests.cs:5`, -`UserRegistrationTests.cs:5`); its `ProfileManagementTests` derives `E2ETestBase` directly +`UserRegistrationTests.cs:5`, `PasswordResetTests.cs:5`); its `ProfileManagementTests` derives +`E2ETestBase` directly (`ProfileManagementTests.cs:8`) because the ADC profile page supports only password change and account deletion, so it does not inherit the base's profile scan. Beyond the Identity bases each app carries a dedicated suite: ADC's `Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/AccessibilityTests.cs:17` holds 31 page -scans (11 through `ScanGridAsync`, 20 strict) and Store's +scans (10 through `ScanGridAsync`, 21 strict) and Store's `Tests/E2E/MMCA.Store.E2E.Tests/Workflows/AccessibilityTests.cs:17` holds 23 (6 grid, 17 strict). **MMCA.Helpdesk adopts none of it**: it pins the package version (`MMCA.Helpdesk/Directory.Packages.props:66`) but no project references it, and the repo has no E2E @@ -107,9 +120,9 @@ accessibility gate today. findings gets demoted to advisory itself. Scoping to the four WCAG tags means every failure is a real conformance violation, which is the precondition for making the check required rather than informational. - **Assert in the base, not in the consumer.** The same push-left posture as ADR-058: shipping the - assertion inside `UserLoginTestsBase` / `UserRegistrationTestsBase` / `ProfileManagementTestsBase` - means an app that adopts the Identity workflow tests cannot accidentally adopt them without the a11y - scan. Opting out is visible (it means not subclassing the base), the way ADC's profile page is. + assertion inside `UserLoginTestsBase` / `UserRegistrationTestsBase` / `ProfileManagementTestsBase` / + `PasswordResetTestsBase` means an app that adopts the Identity workflow tests cannot accidentally adopt + them without the a11y scan. Opting out is visible (it means not subclassing the base), the way ADC's profile page is. - **A real engine, not reflection.** Contrast, accessible names, and live-region announcement exist only in a rendered accessibility tree. That is why this sits in the E2E package with Playwright and not in the NetArchTest tier (ADR-015) or the booted-host conformance tier (ADR-058). diff --git a/docs-src/adr/065-scaffolding-templates.md b/docs-src/adr/065-scaffolding-templates.md index 4a80727..9ef542e 100644 --- a/docs-src/adr/065-scaffolding-templates.md +++ b/docs-src/adr/065-scaffolding-templates.md @@ -7,7 +7,10 @@ guide reference were refreshed against HEAD. Revised 2026-08-18: the seed measur against MMCA.Helpdesk with the counting method now stated inline (`Directory.Packages.props` has since been pruned to 72 lines and 58 pins, and the file and line totals moved with the tree), the staged analyzer delta and the wire-contract removal were re-anchored, and the seed's fitness map -declares 20 sealed subclasses rather than 19. +declares 20 sealed subclasses rather than 19. Revised 2026-08-23: the seed measurements were re-taken +at HEAD (three standalone fitness-test files landed in the architecture-test project since the +previous re-measure, moving the file and line totals), and the generated-app test figure is now +sourced to the seed's own documented run rather than to an undated absolute pair. ## Context @@ -21,10 +24,10 @@ is now the six-step `dotnet new install MMCA.Templates` path (`common-GETTING-STARTED.md:10-22`) and the by-hand transcription moved to its own guide. Measured against MMCA.Helpdesk, the deliberately minimal seed, a brand-new app on the framework -starts by hand-creating **12 projects, 111 files, and 7,671 lines** before a line of its own -business logic. The method, re-run on 2026-08-18 and stated here so the figures can be reproduced +starts by hand-creating **12 projects, 114 files, and 7,740 lines** before a line of its own +business logic. The method, re-run on 2026-08-23 and stated here so the figures can be reproduced rather than trusted: every file under `Source/` and `Tests/` excluding `bin/`, `obj/` and -`TestResults/` (104 files, 6,569 lines), plus the seven root build files below (1,102 lines). Those +`TestResults/` (107 files, 6,638 lines), plus the seven root build files below (1,102 lines). Those seven are an 823-line `.editorconfig`, a 72-line `Directory.Packages.props` carrying 58 pins, a 75-line `Directory.Build.props`, the 74-line local-source swap in `Directory.Build.targets`, `global.json`, `nuget.config`, and the solution file. Under `Source/` and `Tests/` sit the module @@ -165,6 +168,6 @@ silently: the output still compiles, it just carries someone else's domain vocab contract. The delta is exactly one because `IntegrationEventContractTestsBase` declares a single `[Fact]` (`MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/Bases/IntegrationEventContractTestsBase.cs:18-19`). - The absolute figure was 90 against 91 as of 2026-08-02 and is not pinned by any gate (both - `smoke.ps1:318` and `ci.yml:45` pass `--minimum-expected-tests 1`); a generated app that turns off - shape axes runs fewer still. + The seed's own documented run is 100 tests (`MMCA.Helpdesk/CLAUDE.md:109`), one of which the + generated app does not carry. No gate pins either figure (both `smoke.ps1:318` and `ci.yml:45` + pass `--minimum-expected-tests 1`); a generated app that turns off shape axes runs fewer still. diff --git a/docs-src/adr/066-broker-transport-selection.md b/docs-src/adr/066-broker-transport-selection.md index 2a9bd1a..4009ab4 100644 --- a/docs-src/adr/066-broker-transport-selection.md +++ b/docs-src/adr/066-broker-transport-selection.md @@ -28,9 +28,9 @@ carry a dedicated test tier for the transport that only production uses. (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Settings/MessageBusSettings.cs:116-132`), bound from the `MessageBus` section (`:14`) and defaulting to `InProcess` (`:17`). `AddBrokerMessaging` returns the container untouched for `InProcess` - (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:656-659`); for either - broker value it replaces `IMessageBus` with `BrokerMessageBus` (`:676`) and `IEventBus` with - `BrokerEventBus` (`:682`), so the outbox becomes the only delivery channel. No application or + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:669-672`); for either + broker value it replaces `IMessageBus` with `BrokerMessageBus` (`:689`) and `IEventBus` with + `BrokerEventBus` (`:695`), so the outbox becomes the only delivery channel. No application or domain code names a transport. - **Local development is RabbitMQ, wired by the AppHost.** `AddMessageBroker()` provisions the RabbitMQ container with the management plugin @@ -42,38 +42,42 @@ carry a dedicated test tier for the transport that only production uses. `:44`). The developer sets nothing: the orchestrator owns the choice. - **Production is Azure Service Bus, injected by Bicep.** Each service container app receives `MessageBus__Provider=AzureServiceBus` plus a `MessageBus__ConnectionString` secret reference: - ADC at `MMCA.ADC/infra/main.bicep:1124-1125` (identity, `:1018`), `:1293` (conference, `:1219`), - `:1419` (engagement, `:1343`), `:1555` (notification, `:1467`); Store at - `MMCA.Store/infra/main.bicep:1023-1024` (identity, `:939`), `:1156` (catalog, `:1091`), `:1275` - (sales, `:1190`). The images are the same ones the AppHost runs locally; only the two environment variables + ADC at `MMCA.ADC/infra/main.bicep:1136-1137` (identity, `:1029`), `:1315` (conference, `:1236`), + `:1446` (engagement, `:1365`), `:1587` (notification, `:1494`); Store at + `MMCA.Store/infra/main.bicep:1035-1036` (identity, `:950`), `:1175` (catalog, `:1109`), `:1300` + (sales, `:1214`). The images are the same ones the AppHost runs locally; only the two environment variables differ. - **One resolution order for the connection string.** `ResolveBrokerConnectionString` prefers an explicit `MessageBus:ConnectionString`, then `ConnectionStrings:rabbitmq`, then - `ConnectionStrings:messaging` (`DependencyInjection.cs:750-759`). Aspire's `WithReference` and + `ConnectionStrings:messaging` (`DependencyInjection.cs:768-777`). Aspire's `WithReference` and Bicep's `secretRef` therefore both land on a path the host already reads, and the transport selector stays separate from the credential. - **Retry policy is identical on both transports.** Each branch of `ConfigureBrokerTransport` calls `cfg.UseMessageRetry(r => r.Exponential(...))` with the same four arguments before - `ConfigureEndpoints`: RabbitMQ at `DependencyInjection.cs:822-826`, Azure Service Bus at - `:849-853`. The values come from one settings object: `RetryLimit` 5, `RetryMinIntervalSeconds` 1, - `RetryMaxIntervalSeconds` 30 (`MessageBusSettings.cs:43,50,56`). Only in-process retry is - configured, deliberately not `UseDelayedRedelivery`, because that needs the RabbitMQ - delayed-message-exchange plugin the Aspire container does not ship - (`DependencyInjection.cs:769-774`). + `ConfigureEndpoints`: RabbitMQ at `DependencyInjection.cs:835-839`, Azure Service Bus at + `:862-866`. The values come from one settings object: `RetryLimit` 5, `RetryMinIntervalSeconds` 1, + `RetryMaxIntervalSeconds` 30 (`MessageBusSettings.cs:43,50,56`). Second-level redelivery is the one + place the two transports diverge by design: Azure Service Bus schedules messages natively, so + `UseDelayedRedelivery` is applied unconditionally there (`DependencyInjection.cs:859`), while + RabbitMQ keeps it opt-in behind `EnableDelayedRedelivery` (default `false`, + `MessageBusSettings.cs:96`) because it needs the delayed-message-exchange plugin the Aspire + container does not ship (`DependencyInjection.cs:822-825`, posture documented in the remarks at + `:794-801`). - **Service Bus Standard tier and `Manage` rights are forced by the topology MassTransit builds.** - Both namespaces are `Standard`/`Standard` (`MMCA.ADC/infra/main.bicep:713-716`, - `MMCA.Store/infra/main.bicep:665-668`) because `UsingAzureServiceBus` configures a topic per + Both namespaces are `Standard`/`Standard` (`MMCA.ADC/infra/main.bicep:724-727`, + `MMCA.Store/infra/main.bicep:676-679`) because `UsingAzureServiceBus` configures a topic per message type plus a subscription per consumer, and Basic supports queues only - (`MMCA.ADC/infra/main.bicep:704-705`, `MMCA.Store/infra/main.bicep:656-660`). The `app-clients` - authorization rule carries `Send` + `Listen` + **`Manage`** (`MMCA.ADC/infra/main.bicep:728-738`, - `MMCA.Store/infra/main.bicep:680-690`) so `ConfigureEndpoints` can provision that topology at - startup; without `Manage` the first publish fails with an Unauthorized topology error - (`MMCA.ADC/infra/main.bicep:725-726`). Store sources the connection string from that dedicated rule + (`MMCA.ADC/infra/main.bicep:715-716`, `MMCA.Store/infra/main.bicep:667-670`). The `app-clients` + authorization rule carries `Send` + `Listen` + **`Manage`** (`MMCA.ADC/infra/main.bicep:744-746`, + rule at `:739-749`; `MMCA.Store/infra/main.bicep:695-698`, rule at `:691-701`) so + `ConfigureEndpoints` can provision that topology at startup; without `Manage` the first publish + fails with an Unauthorized topology error (`MMCA.ADC/infra/main.bicep:736`, + `MMCA.Store/infra/main.bicep:688`). Store sources the connection string from that dedicated rule rather than `RootManageSharedAccessKey`, so a later move to managed identity can revoke it without touching the namespace root (`MMCA.Store/infra/main.bicep:136,138`). - **Tests use the transport the tier is testing.** A per-service integration host configures no provider, so `AddBrokerMessaging` short-circuits and the in-process bus stands - (`DependencyInjection.cs:656-659`). The cross-service round-trip tier runs the real broker: the + (`DependencyInjection.cs:669-672`). The cross-service round-trip tier runs the real broker: the shared fixture base sets `MessageBus__Provider=RabbitMq` plus `ConnectionStrings__rabbitmq` against a Testcontainers RabbitMQ for every host it boots (`MMCA.Common/Source/Hosting/MMCA.Common.Testing/CrossServiceFixtureBase.cs:249-250`). @@ -81,7 +85,7 @@ carry a dedicated test tier for the transport that only production uses. `mcr.microsoft.com/azure-messaging/servicebus-emulator:2.0.1` (`MMCA.ADC/Tests/Integration/MMCA.ADC.ServiceBusEmulator.IntegrationTests/Infrastructure/ServiceBusEmulatorFixture.cs:50`) and starts a MassTransit v8 bus through `Bus.Factory.CreateUsingAzureServiceBus` with the - custom-clients `Host` overload, the only v8 path onto the emulator (`:119-141`); its static + custom-clients `Host` overload, the only v8 path onto the emulator (`:120-142`); its static constructor lowers MassTransit's process-global TTL and auto-delete defaults under the emulator's one-hour quota (`:64-71`), which is why the tier runs in its own test process. Store carries the parallel tier with the bus created in the test class @@ -94,7 +98,7 @@ carry a dedicated test tier for the transport that only production uses. Adoption differs by repo. ADC and Store select a broker on every extracted service, local and production alike. MMCA.Helpdesk stays on `InProcess`: its monolith host calls the same `AddBrokerMessaging(builder.Configuration)` with no `MessageBus:Provider` configured -(`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:97-99`), and its AppHost provisions no +(`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:105`), and its AppHost provisions no broker, so extraction later is an AppHost change rather than a code change. ## Rationale @@ -122,7 +126,7 @@ broker, so extraction later is an AppHost change rather than a code change. ## Trade-offs - **Two brokers means two behaviors to keep aligned.** Configuration parity is enforced by one code path, but the products still differ (Service Bus supports delayed redelivery natively, the Aspire - RabbitMQ container does not, `DependencyInjection.cs:769-774`), so a transport-specific behavior + RabbitMQ container does not, `DependencyInjection.cs:794-801`), so a transport-specific behavior can still be adopted by accident. - **Production runs a transport that no gating check exercises.** The emulator tiers are `continue-on-error` on a nightly schedule (`MMCA.ADC/.github/workflows/cross-service-tests.yml:150`, @@ -133,10 +137,10 @@ broker, so extraction later is an AppHost change rather than a code change. smoke proves the binding and the topology provisioning, not production behavior at volume. - **`Manage` rights are broad.** The `app-clients` rule can create and delete entities in the namespace, which is the price of letting `ConfigureEndpoints` build the topology instead of - declaring every topic in Bicep (`MMCA.ADC/infra/main.bicep:722-727`). + declaring every topic in Bicep (`MMCA.ADC/infra/main.bicep:739-749`). - **Provider selection is per host and silent when missing.** A service that never receives `MessageBus__Provider` keeps the in-process bus and publishes nothing to the broker, without an - error (`DependencyInjection.cs:656-659`); correctness depends on auditing the AppHost and the Bicep + error (`DependencyInjection.cs:669-672`); correctness depends on auditing the AppHost and the Bicep env lists, the same inventory caveat ADR-021 carries for the inbox. - **The choice lives in AppHost prose that has to be maintained alongside the calls.** The note above ADC's Notification registration now states that `WithBroker(rabbit)` wires the transport the same diff --git a/docs-src/adr/067-ui-module-shell-composition.md b/docs-src/adr/067-ui-module-shell-composition.md index cfc0783..3d592b7 100644 --- a/docs-src/adr/067-ui-module-shell-composition.md +++ b/docs-src/adr/067-ui-module-shell-composition.md @@ -48,11 +48,11 @@ Ship the application shell in the framework package and let each module plug int - **The nav menu is assembled from the registrations, trimmed per user.** `NavMenu` injects the same enumeration (`MMCA.Common.UI/Layout/NavMenu.razor:8`), flattens every module's `NavItems`, drops items whose `RequiredRole` or `RequiredClaim` the current principal does not carry, and splits the - remainder into the General, My Account and Administration sections (`:185-195`). + remainder into the General, My Account and Administration sections (`:196-204`). - **Two component extension points render module-supplied types.** `MainLayout` reads `AppBarComponentTypes` and `LayoutComponentTypes` off the registrations (`MMCA.Common.UI/Layout/MainLayout.razor:98-99`) and renders them through `DynamicComponent` in the - top app bar (`:41-44`, mirrored on the mobile top row at `NavMenu.razor:32-35`) and at the root of + top app bar (`:41-44`, mirrored on the mobile top row at `NavMenu.razor:41-43`) and at the root of the layout (`:80-83`), so a module can add an icon with a badge or a drawer without touching the layout. - **Registration is one call.** `AddUIModule()` runs the Scrutor scan for the module's entity diff --git a/docs-src/adr/068-value-objects-as-validated-primitives.md b/docs-src/adr/068-value-objects-as-validated-primitives.md index c0d431c..2cdd5e3 100644 --- a/docs-src/adr/068-value-objects-as-validated-primitives.md +++ b/docs-src/adr/068-value-objects-as-validated-primitives.md @@ -86,10 +86,11 @@ Model a domain value that carries an invariant as an **immutable record value ob - **gRPC is mapped by hand, not inferred.** `Money` crosses a service boundary as a purpose-built `MoneyV1` message with a **string** amount (proto has no decimal) and a currency code (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Contracts/Protos/product_variants.proto:70,73`), - translated by `MoneyToWire`/`MoneyFromWire` - (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Contracts/ProductVariantServiceGrpcAdapter.cs:126`), - which honors the empty-code sentinel only when the amount is also zero and returns null for a - malformed entry rather than failing the whole batch (`:135-140`). + translated by `MoneyFromWire` + (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Contracts/ProductVariantServiceGrpcAdapter.cs:126`) + and `MoneyToWire` (`:157`). + `MoneyFromWire` honors the empty-code sentinel only when the amount is also zero and returns null + for a malformed entry rather than failing the whole batch (`:135-140`). - **Adoption is real but partial.** Store maps `ProductVariant.Price` (`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Domain/Products/ProductVariant.cs:21`) with `OwnsMoney` (`.../Catalog.Infrastructure/Persistence/EntityConfiguration/ProductVariantConfiguration.cs:31`), @@ -99,7 +100,8 @@ Model a domain value that carries an invariant as an **immutable record value ob (`OrderLineConfiguration.cs:28`). Store Identity maps `Customer.Email` through `EmailValueConverter` and `Customer.Address` through a hand-rolled `OwnsOne` block (`CustomerConfiguration.cs:36,43`), and `User.Email` the same way (`UserConfiguration.cs:24`). ADC types `User.Email` as `Email` - (`MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Domain/Users/User.cs:38`, built at `:164`) with + (`MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Domain/Users/User.cs:38`, validated through + `Email.Create` at `:171` and passed to the constructor at `:185`) with the same converter (`.../Identity.Infrastructure/.../UserConfiguration.cs:21`) and a speaker's optional email through `NullableEmailValueConverter` (`.../Conference.Infrastructure/.../SpeakerConfiguration.cs:43`). `Money` and `Email` are the two diff --git a/docs-src/adr/069-shared-data-protection-key-ring.md b/docs-src/adr/069-shared-data-protection-key-ring.md index cfeaf01..aca712d 100644 --- a/docs-src/adr/069-shared-data-protection-key-ring.md +++ b/docs-src/adr/069-shared-data-protection-key-ring.md @@ -17,9 +17,9 @@ HttpOnly cookies read during Blazor SSR prerender, and the Blazor Server forms t antiforgery tokens; both are DataProtection payloads. ADR-008 then split the monolith into independently scaled hosts, and in ADC the two hosts that mint those payloads (the UI host and the Identity service, which also does OAuth correlation and state cookie cryptography) both run at -`maxReplicas: 2` (`MMCA.ADC/infra/main.bicep:1211` Identity service, `:1826` UI host). Only the +`maxReplicas: 2` (`MMCA.ADC/infra/main.bicep:1228` Identity service, `:1860` UI host). Only the Identity service runs with **no session affinity**; the UI ingress is sticky -(`MMCA.ADC/infra/main.bicep:1749-1751`), which narrows the UI window rather than closing it, since +(`MMCA.ADC/infra/main.bicep:1782-1783`), which narrows the UI window rather than closing it, since affinity is lost on a replica restart, a revision swap, or a dropped affinity cookie. Nothing in the record decided **where the key ring lives**. ADR-061 decides how a running app reaches @@ -57,12 +57,12 @@ Azure blob so every replica of a host shares one ring Crypto User role, because that role assignment is granted out of band and can lag a deployment. Folding the second step into the first would turn an optional hardening gap into a total authentication outage. The deployment template records the same reasoning as a follow-up - (`MMCA.ADC/infra/main.bicep:836-839`), and no deployed app sets `DataProtection__KeyVaultKeyUri` + (`MMCA.ADC/infra/main.bicep:847-850`), and no deployed app sets `DataProtection__KeyVaultKeyUri` today, so gate 2 is configured nowhere: the key ring is persisted but not encrypted at rest. - **One `DefaultAzureCredential` instance serves both sinks** (`DataProtectionExtensions.cs:68`), so they share a single token cache. A deployed host authenticates with its managed identity and a developer machine falls back to the local Azure CLI or Visual Studio sign-in; ADC pins **which** - identity with `AZURE_CLIENT_ID` on both adopting apps (`MMCA.ADC/infra/main.bicep:1136`, `:1782`). + identity with `AZURE_CLIENT_ID` on both adopting apps (`MMCA.ADC/infra/main.bicep:1148`, `:1816`). - **ADC adopts it on exactly the two hosts that mint the payloads.** The Identity service calls it (`MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:126`) and so does the Web UI host (`MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:40`). Neither call sits immediately after @@ -74,11 +74,11 @@ Azure blob so every replica of a host shares one ring token. - **In ADC, infrastructure provisions one private container, not a new storage account.** `dataprotection-keys` is created on the existing avatar storage account with `publicAccess: 'None'` - (`MMCA.ADC/infra/main.bicep:821-827`), deliberately unlike the public `avatars` container beside it, + (`MMCA.ADC/infra/main.bicep:832-838`), deliberately unlike the public `avatars` container beside it, and both apps are pointed at `.../dataprotection-keys/keys.xml` with the shared discriminator - `MMCA.ADC` (`:1134-1135`, `:1780-1781`), unconditionally. No extra role assignment is needed: the + `MMCA.ADC` (`:1146-1147`, `:1814-1815`), unconditionally. No extra role assignment is needed: the ADR-045 Storage Blob Data Contributor grant is scoped to the storage **account**, so it already - covers this container (`:834-835`). That grant is itself guarded by `grantAvatarStorageRole`, + covers this container (`:845-846`). That grant is itself guarded by `grantAvatarStorageRole`, default `false`, because the deploy identity deliberately lacks role-assignment rights (`:113`, `:840-848`). - **The Azure dependencies live in the Aspire package only.** @@ -90,19 +90,19 @@ Azure blob so every replica of a host shares one ring **Both consumers have now adopted it (2026-08-13).** MMCA.Store originally had no call site and no `DataProtection` configuration anywhere in the repo, even though its UI and Identity container apps -also run at `maxReplicas: 2` (`MMCA.Store/infra/main.bicep:1481` UI host, `:1083` Identity +also run at `maxReplicas: 2` (`MMCA.Store/infra/main.bicep:1513` UI host, `:1101` Identity service). Its UI host now calls `AddCommonDataProtection()` immediately after `AddServiceDefaults()` (`MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/Program.cs:76`, `:82`), and the infrastructure side has landed and is live. Store diverges from ADC in three ways worth recording: - **A new dedicated storage account, not a reused one.** Store has no public-blob workload to share an account with, so the template provisions its own `Standard_LRS` account `dataProtectionStorage` with - `allowBlobPublicAccess: false` (`MMCA.Store/infra/main.bicep:783-801`) and its own private - `dataprotection-keys` container (`:808-814`). + `allowBlobPublicAccess: false` (`MMCA.Store/infra/main.bicep:794-812`) and its own private + `dataprotection-keys` container (`:819-825`). - **The blob URI is gated behind a readiness flag.** `DataProtection__ApplicationName='MMCA.Store'` - (`:1464`) and `AZURE_CLIENT_ID` (`:1466`) are unconditional, but + (`:1496`) and `AZURE_CLIENT_ID` (`:1498`) are unconditional, but `DataProtection__BlobStorageUri` is appended only when the `dataProtectionStorageReady` parameter is - true (default `false` at `:93`, concatenated at `:1467-1469`). The flag exists because + true (default `false` at `:93`, concatenated at `:1499-1501`). The flag exists because `AddCommonDataProtection` gates on the presence of the URI, never on reachability: wiring the URI before the data-plane grant exists would 403 on the first protect call rather than degrade. That flag has since been flipped true in production: the deploy workflow passes @@ -110,7 +110,7 @@ landed and is live. Store diverges from ADC in three ways worth recording: (`MMCA.Store/.github/workflows/deploy.yml:961`). - **Its own role-assignment guard.** The Storage Blob Data Contributor grant is guarded by Store's own `grantDataProtectionStorageRole` parameter (default `false` at `:90`), with the account-scoped - role assignment at `:832`, deliberately separate from the readiness flag above: one says whether + role assignment at `:843`, deliberately separate from the readiness flag above: one says whether THIS deployment creates the grant, the other says whether the grant already exists. The framework side needed no change at all: the whole delta was one call site plus infrastructure, diff --git a/docs-src/adr/070-fail-fast-configuration-contract.md b/docs-src/adr/070-fail-fast-configuration-contract.md index 7b02918..a2a0a2e 100644 --- a/docs-src/adr/070-fail-fast-configuration-contract.md +++ b/docs-src/adr/070-fail-fast-configuration-contract.md @@ -2,7 +2,11 @@ ## Status Accepted (2026-08-07). Revised 2026-08-14 (source citations re-anchored; the consumer-repo facade -claim narrowed to production code, with the controller-test exception recorded). +claim narrowed to production code, with the controller-test exception recorded). Revised 2026-08-23 +(inventory re-counted after the password-reset vertical and the opt-in feature waves: twelve validated +chains in the Infrastructure package and sixteen framework registrations in all, six framework +bindings deliberately off the chain, a custom `IValidateOptions` now in use, and the +Application-layer "no `IOptions`" invariant recorded as no longer holding). ## Context Every host in the workspace reads a dozen or more configuration sections: connection strings, SMTP, @@ -31,24 +35,31 @@ type through a read-only interface when it must be read above Infrastructure.** is the required form (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:67-70`). `.BindConfiguration(T.SectionName)` is the accepted shorthand for the `Bind(GetSection(...))` step and is what Store's Sales module uses - (`MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.API/SalesModule.cs:47-50`, `:52-55`). + (`MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.API/SalesModule.cs:47-50`, `:52-55`, `:57-60`). `ValidateOnStart()` is the load-bearing link: without it the annotations are evaluated lazily on first resolution, which is exactly the first-use failure the contract exists to prevent. -- **The framework owns the base set.** Eight sections are bound this way in the Infrastructure package's - `DependencyInjection.cs` alone: seven inside `AddInfrastructure` (`ConnectionStringSettings` `:67-70`, - `SmtpSettings` `:81-84`, `PersistenceSettings` `:123-126`, `OutboxSettings` `:128-131`, - `LoginProtectionSettings` `:133-136`, `MessageBusSettings` `:139-142`, `JwksSettings` `:144-147`) and an - eighth in the opt-in `AddPushNotifications` (`PushNotificationSettings` `:530-533`). Three more are bound - by the Presentation packages: `IdempotencySettings` +- **The framework owns the base set.** Twelve sections are bound this way in the Infrastructure package's + `DependencyInjection.cs` alone. Eight are inside `AddInfrastructure` (`ConnectionStringSettings` + `:67-70`, `SmtpSettings` `:81-84`, `PersistenceSettings` `:123-126`, `OutboxSettings` `:128-131`, + `LoginProtectionSettings` `:133-136`, `PasswordResetSettings` `:139-142`, `MessageBusSettings` + `:145-148`, `JwksSettings` `:150-153`), so every host gets them by registering the package. The other + four sit in opt-in registration methods a host calls only when it wants the feature: `SchedulerSettings` + in `AddScheduledJobs` (`:319-322`), `AuditTrailSettings` in `AddAuditTrail` (`:390-393`), + `TenancySettings` in `AddMultiTenancy` (`:439-442`), and `PushNotificationSettings` in + `AddPushNotifications` (`:543-546`). Four more are bound outside that file: `IdempotencySettings` (`MMCA.Common/Source/Presentation/MMCA.Common.API/DependencyInjection.cs:70-73`), `JwtSettings`, bound inside `AddCommonAuthentication` - (`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs:502-505`), - and `ApiSettings` (`MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:32-35`). + (`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs:538-541`, + the method starting at `:536`), `ApiSettings` + (`MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:32-35`), and + `GatewayRateLimitingSettings`, bound by the Aspire hosting package's `AddGatewayRateLimiting` + (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Gateway/GatewayRateLimitingExtensions.cs:148-151`). + Sixteen framework registrations in all. - **Each service host adds exactly two of its own**, `ApplicationSettings` and `ModulesSettings`, and all eight hosts across the three application repos do it identically: ADC's four services (for example - `MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:170-173` and `:313-316`), Store's three - (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:123-126` and `:226-229`), and the - Helpdesk monolith seed (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:17-20` and `:75-78`). + `MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Program.cs:170-173` and `:320-323`), Store's three + (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:123-126` and `:228-231`), and the + Helpdesk monolith seed (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:17-20` and `:81-84`). Modules may add their own sections on the same chain, as Store's Sales module does for Stripe. - **Validation is data annotations, extended by `IValidatableObject` where a rule spans fields.** `JwtSettings` marks `Issuer` and `Audience` `[Required]` @@ -59,28 +70,45 @@ type through a read-only interface when it must be read above Infrastructure.** fails to boot rather than failing to sign its first token. - **Read-only interface facades over `IOptions`.** Five settings types are additionally registered as singletons that resolve `IOptions.Value` and hand back an interface: `IApplicationSettings` - (`MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:31`), `IJwtSettings` + (`MMCA.Common/Source/Core/MMCA.Common.Application/DependencyInjection.cs:32`), `IJwtSettings` (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:65`), - `IConnectionStringSettings` (`:71`), `ISmtpSettings` (`:85`), and `IPushNotificationSettings` (`:534-535`). + `IConnectionStringSettings` (`:71`), `ISmtpSettings` (`:85`), and `IPushNotificationSettings` (`:547-548`). The interfaces declare `get`/`init` members only (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Settings/ISmtpSettings.cs:6-28`), so a consumer can read a setting and cannot rebind it. The interface for `IJwtSettings` lives in a file named `IJwSettings.cs` (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Settings/IJwSettings.cs:10`); the type name, not the filename, is the contract. -- **Three recorded exceptions to the chain, all in the framework.** `CacheKeyPrefixOptions` is bound with - a bare `services.Configure` and no validation, because an absent section must leave cache keys exactly - as callers write them (a single call inside `AddCaching`, - `MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:172`, the method starting - at `:164`). - `LayoutSettings` is bound without validation for the same reason: it is optional footer copy - (`MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:38-39`). `DataSourcesSettings` is +- **Six recorded exceptions in the framework, on one shared reason: an absent section is a working + default, not a misconfiguration.** `CacheKeyPrefixOptions` is bound with a bare `services.Configure` + and no validation, because an absent section must leave cache keys exactly as callers write them (a + single call inside `AddCaching`, + `MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:185`, the method starting + at `:177`). + `LayoutSettings` binds with `AddOptions().Bind()` and nothing after it for the same reason: it is + optional footer copy (`MMCA.Common/Source/Presentation/MMCA.Common.UI/DependencyInjection.cs:38-39`). + `NativePushSettings` (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:578-579`) + and `FileStorageSettings` (`:610-611`) bind the same way inside their opt-in registration methods, + both of which read the section back and turn themselves into a no-op when it is absent or incomplete, + so a host registers them unconditionally and switches the feature on by configuration alone. + `SecurityHeadersSettings` binds through an options builder that calls `.Bind` only when configuration + was supplied at all, because the same call accepts a code-only `Action` + instead (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Security/SecurityHeaders.cs:170-174`). + `DataSourcesSettings` is the one exception with a mechanical rather than a policy reason: it is constructed directly from the section rather than through `AddOptions`, because a root-level dictionary section does not bind through the options pipeline (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:75-77`). +- **Consumer hosts take the same escape twice, and write the reason at the call site.** ADC's Engagement + service binds `PointsSettings` + (`MMCA.ADC/Source/Services/MMCA.ADC.Engagement.Service/Program.cs:146-147`) and `CheckInSettings` + (`:153-154`) without `ValidateOnStart`: the defaults are working values and an explicit zero is the + documented per-rule kill switch, so no value an organizer could set should stop that host from booting + mid-conference. Both follow the rule the framework's six follow: an exception is legitimate when the + section has no invalid value, and the reason belongs in a comment beside the binding rather than left + to be inferred from its absence. **Adoption of the facade half is partial, and this ADR settles the direction rather than claiming the state.** Only five of the framework's settings types have a facade; the rest are consumed as `IOptions` -inside Infrastructure and API, which is where they belong: `OutboxProcessor` +at the point of use, which inside Infrastructure and API is where they belong: `OutboxProcessor` (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Outbox/OutboxProcessor.cs:57`), `BrokerEventBus` (`.../Services/BrokerEventBus.cs:34`), `LoginProtectionService` (`.../Auth/LoginProtectionService.cs:21`), `RsaJwksProvider` (`.../Auth/RsaJwksProvider.cs:15`), @@ -88,7 +116,7 @@ inside Infrastructure and API, which is where they belong: `OutboxProcessor` (`MMCA.Common/Source/Presentation/MMCA.Common.API/Idempotency/IdempotencyFilter.cs:432`). The facades are consumed only inside the framework: `EntityControllerBase` resolves `IApplicationSettings` per request at two sites, its `MaxPageSize` and `MaxExportRows` accessors -(`MMCA.Common/Source/Presentation/MMCA.Common.API/Controllers/EntityControllerBase.cs:60` and `:80`), +(`MMCA.Common/Source/Presentation/MMCA.Common.API/Controllers/EntityControllerBase.cs:61` and `:81`), `RepositoryFactory` takes it in its constructor (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Repositories/Factory/RepositoryFactory.cs:14`), `TokenService` takes `IJwtSettings` (`.../Services/TokenService.cs:47`), and `SmtpEmailSender` takes @@ -98,7 +126,7 @@ facade: nothing under `Source/` in ADC, Store, or Helpdesk references `IApplicat because of `EntityControllerBase`: Store registers a stand-in `IApplicationSettings` in six API test fixtures so `MaxPageSize` resolves (for example `MMCA.Store/Tests/Modules/Sales/MMCA.Store.Sales.API.Tests/Controllers/ShoppingCartsControllerTests.cs:44`), -and six ADC Conference controller tests name it only in a comment recording that an empty provider yields +and eight ADC Conference controller tests name it only in a comment recording that an empty provider yields the default (for example `MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.API.Tests/Controllers/SponsorsControllerTests.cs:41`); Helpdesk has no reference of either kind. @@ -108,13 +136,35 @@ in `StripeClientFactory.cs:21`, `StripePaymentService.cs:62`, `StripeWebhookRegi `MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Program.cs:145`, and its calendar component in `MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.UI/Components/AddToCalendarButton.razor:7`). -The Application-layer invariant nevertheless holds today: no Application-layer file in any of the four -repos takes an `IOptions` dependency, because settings are read at the Infrastructure, API, and host -edges. The settled direction is therefore narrower than "never use `IOptions`": the **fail-fast chain is -mandatory for every section** other than the three recorded exceptions, and the **facade is the required +The Application layer no longer keeps clear of the options pipeline. Seven Application-layer files take +an `IOptions` constructor dependency today, and the framework itself supplies the first: the shared +`ForgotPasswordHandlerBase` takes `IOptions` +(`MMCA.Common/Source/Core/MMCA.Common.Application/Users/UseCases/ForgotPassword/ForgotPasswordHandlerBase.cs:39`). +ADC adds four: its Identity `ForgotPasswordHandler` +(`MMCA.ADC/Source/Modules/Identity/MMCA.ADC.Identity.Application/Users/UseCases/ForgotPassword/ForgotPasswordHandler.cs:24`), +and in Engagement `PointsAwarder` +(`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/Points/Services/PointsAwarder.cs:31`), +`GetLeaderboardHandler` (`.../Points/UseCases/GetLeaderboard/GetLeaderboardHandler.cs:30`) and +`RecordRoomCheckInHandler` (`.../CheckIns/UseCases/RecordRoomCheckIn/RecordRoomCheckInHandler.cs:28`). +Store adds two: `CreateCheckoutSessionCommandValidator` +(`MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.Application/Orders/UseCases/CreateCheckoutSession/CreateCheckoutSessionCommandValidator.cs:23`) +and its Identity `ForgotPasswordHandler` +(`MMCA.Store/Source/Modules/Identity/MMCA.Store.Identity.Application/Users/UseCases/ForgotPassword/ForgotPasswordHandler.cs:25`). +Only Helpdesk's Application layer names no `IOptions` at all. + +The settled direction is therefore narrower than "never use `IOptions`": the **fail-fast chain is +mandatory for every section** other than the exceptions recorded above, and the **facade is the required form only when a settings type must be read above Infrastructure**. Below that boundary `IOptions` at -the point of consumption stays correct, and the direct injections listed above are conformant, not debt, -with the exception that new framework settings crossing into Application or controller code get a facade. +the point of consumption stays correct, and the direct injections in Infrastructure, API and the hosts +listed above are conformant, not debt. The seven Application-layer injections are the part that is not: +each reads a settings section above Infrastructure without a facade, so by this ADR's own rule each is +owed one. `PasswordResetSettings` is the load-bearing case, because the framework's own base class takes +it and both consumer handlers forward their `IOptions` straight into that base +constructor (ADC at `.../MMCA.ADC.Identity.Application/Users/UseCases/ForgotPassword/ForgotPasswordHandler.cs:26`, +Store at `.../MMCA.Store.Identity.Application/Users/UseCases/ForgotPassword/ForgotPasswordHandler.cs:27`): +a facade there removes three of the seven at once. The remaining four injections read three +consumer-owned sections (`PointsSettings` twice, `CheckInSettings`, `CheckoutRedirectSettings`), so those +facades are the consumers' to add. ## Rationale - **A boot failure is cheaper than a first-use failure.** A host that will not start is caught by the @@ -131,7 +181,7 @@ with the exception that new framework settings crossing into Application or cont and `RepositoryFactory` need one number each; taking `IApplicationSettings` rather than `IOptions` keeps `Microsoft.Extensions.Options` out of their signatures, and the `init`-only members make the setting unwritable at the point of use. -- **One shape makes the contract auditable.** Because the chain is textually identical in all eleven +- **One shape makes the contract auditable.** Because the chain is textually identical in all sixteen framework registrations and all sixteen host registrations, a grep for `ValidateOnStart` is a complete inventory of what a host validates at boot. @@ -139,7 +189,9 @@ with the exception that new framework settings crossing into Application or cont - **Nothing enforces it.** There is no architecture fitness test asserting that a new `AddOptions` call carries `ValidateDataAnnotations().ValidateOnStart()`. The uniformity above is convention held by review, not a gate, which is weaker than the invariant-over-discipline posture ADR-015 applies elsewhere. A - section added without the chain fails silently, which is to say it fails later. + section added without the chain fails silently, which is to say it fails later. Six framework bindings + and two consumer-host bindings sit off the chain by choice, and nothing in the build distinguishes + those from a section whose author simply forgot: only the comment beside each one does. - **Bad configuration becomes a crash loop, not a degraded start.** A deployed replica with a missing required value never reaches the warm-up and readiness machinery of ADR-025: it terminates at host build. That is the intended trade (no half-configured replica serves traffic), but it means a configuration @@ -148,7 +200,17 @@ with the exception that new framework settings crossing into Application or cont repo's production code uses a facade at all, so a reader encounters both forms. The boundary above sets which is correct where, but it does not make the codebase look uniform today. - **Data annotations are the vocabulary.** Anything richer needs `IValidatableObject` or a custom - `IValidateOptions`, and only `JwtSettings` reaches for the former today. + `IValidateOptions`, and both escape hatches are in use once each. `JwtSettings` is the only settings + type implementing the former + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Settings/JwtSettings.cs:16`). `TenancySettings` is + the only one with the latter: `TenancySettingsValidator` + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Settings/TenancySettingsValidator.cs:23-24`) is + registered as an `IValidateOptions` beside the chain that binds it + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:445-446`), because + confirming that a tenant's data-source override names a real physical source needs + `IDataSourceResolver`, which no annotation can reach. The cost of the second form is that the rule + leaves the settings type and has to be registered separately, so a host that binds the section without + also registering the validator validates less than it appears to. - **A facade is a snapshot.** Each facade is a singleton resolved from `IOptions.Value`, so it captures the bound instance once and never observes a configuration reload. `IOptions` itself has the same property; a section that genuinely needs reload would have to move to `IOptionsMonitor`, and none diff --git a/docs-src/adr/073-multi-tenancy-model.md b/docs-src/adr/073-multi-tenancy-model.md index f4a1357..ece0828 100644 --- a/docs-src/adr/073-multi-tenancy-model.md +++ b/docs-src/adr/073-multi-tenancy-model.md @@ -11,6 +11,14 @@ field, not four inline arrays). Revised 2026-08-18: that exclusion now runs at **six** call sites in `EFReadRepository`, and the field and every call site were re-anchored again. The count has moved four to five to six across three revisions, which is the point: the shared field is the invariant, the number of read paths using it is not. +Revised 2026-08-23: re-anchored the middleware citations after the edge pipeline moved into a named-step +registry (`MiddlewarePipelineBuilder` plus `MiddlewarePipelineStepNames`, the order frozen by the +`MiddlewarePipelineOrderTestsBase` fitness function rather than by inline calls); corrected the write-side +guard, which is **not** a no-op when no tenant is resolved (an insert of an untenanted `ITenantEntity` from +an untenanted scope throws); restated why `DesignTimeDbContextHelper` registers that interceptor, since +`OnConfiguring` now resolves it with `GetService` and its absence no longer breaks `dotnet ef`; and +replaced the claimed adoption-sweep grep with what is verifiable, namely zero parameterless +`IgnoreQueryFilters()` call sites in `MMCA.Common/Source` today and no automated gate against a future one. ## Context MMCA.Common already partitions data along two axes and neither of them is a tenant. ADR-006 partitions by @@ -75,22 +83,42 @@ per-tenant connection strings. Host-based resolution (tenant from subdomain) is hostname-to-tenant map and certificate handling that no consumer needs today. `TenantResolutionMiddleware` (`Source/Presentation/MMCA.Common.API/Middleware/TenantResolutionMiddleware.cs`) -mirrors `CorrelationIdMiddleware` -(`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:48`) and is wired -into `UseCommonMiddlewarePipeline` (`:45`) immediately **after** `app.UseAuthentication()` (`:96`), -because a claim-first resolution order requires that `HttpContext.User` already be populated. Like -`SoftDeletedUserMiddleware` (`:109`) it is registered unconditionally and inert by default, keeping the -pipeline one shape across every host. When `RequireTenant` is true and nothing resolves on a non-excluded -path, it returns 400 with a ProblemDetails body. +mirrors `CorrelationIdMiddleware`, which is a named step of the same edge pipeline +(`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/MiddlewarePipelineBuilder.cs:40`, its name a +constant at `MiddlewarePipelineStepNames.cs:20`). Tenant resolution is its own step +(`MiddlewarePipelineBuilder.cs:112-118`), placed immediately **after** the `Authentication` step +(`:108-110`), because a claim-first resolution order requires that `HttpContext.User` already be populated. +`UseCommonMiddlewarePipeline` +(`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:46`, with a +`configure` overload at `:58`) applies the steps through `ApplyPipeline` (`:138`), which seeds +`MiddlewarePipelineBuilder.CreateDefault()` (`MiddlewarePipelineBuilder.cs:31`): the order is data, +frozen by the `MiddlewarePipelineOrderTestsBase` fitness function rather than by a sequence of inline +`Use*` calls. Like the `SoftDeletedUserFilter` step (`MiddlewarePipelineBuilder.cs:130`, its name at +`MiddlewarePipelineStepNames.cs:59`) the tenant step is registered unconditionally and inert by default, +keeping the pipeline one shape across every host. When `RequireTenant` is true and nothing resolves on a +non-excluded path, it returns 400 with a ProblemDetails body. ### Writes are guarded by their own interceptor `TenantSaveChangesInterceptor` is a **separate** interceptor from the audit one (one concern per interceptor, matching how audit stamping and domain-event capture are already split at `:236-251`, where it is registered between the two). It stamps `TenantId` on Added entries and throws `CrossTenantWriteException` on any Added, Modified, or Deleted entry whose tenant differs from the -resolved one. It is always registered and is a no-op when no -tenant is resolved. `DesignTimeDbContextHelper` must register it too, or `dotnet ef` breaks for every -consumer the moment it is resolved as a required service. +resolved one. It is always registered +(`TryAddSingleton`, +`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:63`), and with no tenant +resolved it is inert for updates, deletes, and every entity that does not carry `ITenantEntity`: the +system context is unrestricted on the write side exactly as it is on the read side. Inserts are the one +exception. `StampOrVerifyInsert` throws `CrossTenantWriteException.ForUnresolvedTenant` when an Added +`ITenantEntity` declares no tenant of its own and the scope resolved none +(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/Interceptors/TenantSaveChangesInterceptor.cs:107-110`), +because a row that no tenant can ever read is a worse outcome than a failed save; a system scope that +names the tenant explicitly (a seeder, a per-tenant job) still writes. +`DesignTimeDbContextHelper` registers it too +(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/Design/DesignTimeDbContextHelper.cs:77`), +so `dotnet ef` scaffolds against exactly the runtime interceptor pipeline for consumers with and without +tenancy. Its absence is survivable rather than fatal: `OnConfiguring` resolves the tenant interceptor with +`GetService` and falls back to the two-interceptor chain (`ApplicationDbContext.cs:244`), so a +directly-constructed test or design-time context that never registered it still builds. ### The tenant is read live, not copied at context creation `ApplicationDbContext` gains `internal Func? TenantIdAccessor` and @@ -183,8 +211,11 @@ per-tenant database override, so the runnable seed exercises both halves of this ## Trade-offs - **Reads are on discipline where writes are on an invariant.** A consumer calling EF's own parameterless `IgnoreQueryFilters()` on a raw `Table` surface drops the tenant filter along with soft-delete. Writes - remain guarded by `TenantSaveChangesInterceptor` and the adoption sweep includes a grep for the - parameterless form, but nothing prevents a future call site. ADR-055's + remain guarded by `TenantSaveChangesInterceptor`, and `MMCA.Common/Source` carries **zero** parameterless + `IgnoreQueryFilters()` call sites today (the only mentions are the doc comments at + `EFReadRepository.cs:30` and `TenantSaveChangesInterceptor.cs:32` explaining why the named form is used), + but no automated gate holds that count: no architecture rule, fitness test, or CI step checks for the + parameterless form, so nothing fails a build if a future call site appears. ADR-055's `ApplicationLayer_DoesNotUseRawQueryableSurfaces` is partial cover only: it does not reach Infrastructure. - **The EF-style singleton adapter factories are tenant-unaware by design.** `DefaultSqlServerDbContextFactory` and its siblings exist for tooling and adapter scenarios that have no diff --git a/docs-src/adr/077-hybridcache-substrate.md b/docs-src/adr/077-hybridcache-substrate.md index d363b23..db475e2 100644 --- a/docs-src/adr/077-hybridcache-substrate.md +++ b/docs-src/adr/077-hybridcache-substrate.md @@ -62,8 +62,8 @@ window, and a single-pattern eviction would leave them unreachable until the nex ### `ICacheService` gains a default `GetOrCreateAsync` `ICacheService` -(`MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/ICacheService.cs`, `GetAsync` at `:15`, -`SetAsync` at `:24`, `RemoveAsync` at `:34`, `RemoveByPrefixAsync` at `:40`) gains +(`MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/ICacheService.cs`, `GetAsync` at `:17`, +`SetAsync` at `:26`, `RemoveAsync` at `:36`, `RemoveByPrefixAsync` at `:42`) gains `GetOrCreateAsync(key, factory, expiration?, cancellationToken)` as a **default interface member**, following the `IncrementAsync` precedent at `:57` precisely because that precedent proved the shape is non-breaking: no existing implementer, in the framework or in a consumer, has to change. The default @@ -113,14 +113,14 @@ the L1 benefit for free, because their reads go through `GetAsync`. ### Registration is one opt-in call `AddCommonHybridCache(Action? configure = null)` -(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:248`) calls `AddHybridCache` with +(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:261`) calls `AddHybridCache` with defaults matching `CacheOptions` (`LocalCacheExpiration` 30 seconds), then `RemoveAll()` -(`:266`) and an `AddSingleton` **factory** (`:267`), not the two-generic-argument +(`:279`) and an `AddSingleton` **factory** (`:280`), not the two-generic-argument `AddSingleton` form: the lambda resolves the logger (falling back to `NullLogger` when none is registered), the optional `IConnectionMultiplexer` and the `CacheKeyNamespace` itself and hands them to the constructor, rather than leaving the optional parameters to the container. Remove-then-add makes the call order-independent against `AddInfrastructure`, whose `AddCaching` registers -its substrate with `TryAddSingleton` (`:175`): the opt-in wins either way. +its substrate with `TryAddSingleton` (`:188`): the opt-in wins either way. ### Tags are deferred `HybridCache.RemoveByTagAsync` would eventually retire prefix invalidation and its SCAN cost, and it is not diff --git a/docs-src/adr/079-shared-http-middleware-pipeline.md b/docs-src/adr/079-shared-http-middleware-pipeline.md index 963b7d7..edd363a 100644 --- a/docs-src/adr/079-shared-http-middleware-pipeline.md +++ b/docs-src/adr/079-shared-http-middleware-pipeline.md @@ -89,27 +89,31 @@ every REST/gRPC host call it instead of composing its own. `WebApplication` is built, so it runs in the fast unit tier. The framework subclasses it in its own test pass (`Tests/Hosting/MMCA.Common.Testing.Tests/MiddlewarePipelineOrderTests.cs`); consumer repos subclass it next to their decorator-order tests. -- **Conditional middleware is registered unconditionally and made inert by configuration.** Both - `TenantResolutionMiddleware` (`:102`) and `SoftDeletedUserMiddleware` (`:109`) are always in the chain - and gated by their own settings, so the pipeline is literally one shape on every host rather than a - per-host permutation. +- **Conditional middleware is registered unconditionally and made inert at runtime.** Both + `TenantResolutionMiddleware` (the `TenantResolution` step, `MiddlewarePipelineBuilder.cs:112-118`) and + `SoftDeletedUserMiddleware` (the `SoftDeletedUserFilter` step, `:128-130`) are always in the chain: + the first passes the request straight through unless `Tenancy:Enabled` is set + (`Middleware/TenantResolutionMiddleware.cs:62`), the second resolves `ISoftDeletedUserValidator` + lazily and no-ops where no implementation is registered + (`Middleware/SoftDeletedUserMiddleware.cs:43-50`), so the pipeline is literally one shape on every + host rather than a per-host permutation. - **Every REST/gRPC host calls it.** All seven extracted services in the two production apps: ADC Identity (`MMCA.ADC/Source/Services/MMCA.ADC.Identity.Service/Program.cs:322`), ADC Conference - (`MMCA.ADC.Conference.Service/Program.cs:392`), ADC Engagement - (`MMCA.ADC.Engagement.Service/Program.cs:328`), ADC Notification - (`MMCA.ADC.Notification.Service/Program.cs:258`), Store Catalog - (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:266`), Store Identity + (`MMCA.ADC.Conference.Service/Program.cs:399`), ADC Engagement + (`MMCA.ADC.Engagement.Service/Program.cs:334`), ADC Notification + (`MMCA.ADC.Notification.Service/Program.cs:264`), Store Catalog + (`MMCA.Store/Source/Services/MMCA.Store.Catalog.Service/Program.cs:268`), Store Identity (`MMCA.Store.Identity.Service/Program.cs:257`) and Store Sales - (`MMCA.Store.Sales.Service/Program.cs:275`). The reference app calls it too - (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:111`), and because that tree **is** the + (`MMCA.Store.Sales.Service/Program.cs:277`). The reference app calls it too + (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:117`), and because that tree **is** the `mmca-app` template (`MMCA.Helpdesk/.template.config/template.json:5,7`, ADR-065), a scaffolded app gets the same line: the generated `MMCA.ECommerce` sample has it at `MMCA.ECommerce/Source/Hosts/MMCA.ECommerce.Web/Program.cs:100`. - **Hosts extend it by appending, after the call, or through the builder.** Service hosts map their extra endpoints below the one line: OpenAPI outside Production - (`MMCA.ADC.Notification.Service/Program.cs:265-268`), the SignalR hub (`:275`, which + (`MMCA.ADC.Notification.Service/Program.cs:271-274`), the SignalR hub (`:281`, which `SignalRExtensions.cs:18-19` documents as "call after `UseCommonMiddlewarePipeline`"), and gRPC - services (`:277-279`). A host that needs a change inside the edge uses the configure overload + services (`:286` and `:294`). A host that needs a change inside the edge uses the configure overload instead; no host does today, and every one of the eight production and reference hosts calls the zero-argument overload. @@ -189,7 +193,7 @@ and queries), [ADR-019](019-rate-limiting.md) (depends on forwarded headers befo limiter after authentication), [ADR-047](047-soft-deleted-user-session-revocation.md) (depends on the slot between authentication and authorization), [ADR-073](073-multi-tenancy-model.md) (depends on the slot immediately after authentication), [ADR-027](027-multi-locale-i18n.md) (the request localization this -pipeline wires at `:53`), [ADR-012](012-grpc-host-transport.md) (the h2c transport the HTTPS-redirect +pipeline wires at `:47`), [ADR-012](012-grpc-host-transport.md) (the h2c transport the HTTPS-redirect exemption exists for), [ADR-040](040-authenticated-output-caching-for-public-reads.md) (the output cache -at `:111`), [ADR-023](023-security-response-headers.md) (the edge middleware deliberately NOT in this +at `:136-138`), [ADR-023](023-security-response-headers.md) (the edge middleware deliberately NOT in this pipeline), [ADR-008](008-service-extraction-topology.md) (the extraction path this shared edge preserves). diff --git a/docs-src/adr/080-deploy-rollout-revision-rollback.md b/docs-src/adr/080-deploy-rollout-revision-rollback.md index 2fbf3a1..2f0d05b 100644 --- a/docs-src/adr/080-deploy-rollout-revision-rollback.md +++ b/docs-src/adr/080-deploy-rollout-revision-rollback.md @@ -34,15 +34,15 @@ verification fails. - **Single-revision rollout.** Every container app runs `activeRevisionsMode: 'Single'`, so a deploy replaces the serving revision rather than splitting traffic across two: Store's identity, catalog, - sales, gateway and ui apps (`MMCA.Store/infra/main.bicep:952,1104,1203,1334,1419`) and ADC's + sales, gateway and ui apps (`MMCA.Store/infra/main.bicep:963,1122,1227,1364,1450`) and ADC's identity, conference, engagement, notification, gateway and ui apps - (`MMCA.ADC/infra/main.bicep:1031,1232,1356,1480,1641,1743`). There is no canary or blue/green stage + (`MMCA.ADC/infra/main.bicep:1042,1249,1378,1507,1673,1776`). There is no canary or blue/green stage and no traffic-splitting step. - **Readiness gating is the first line of defence.** Every app carries startup, liveness and readiness probes, with readiness on `/health/ready`, so ACA holds user traffic on the old revision - until the new one is warm (`MMCA.Store/infra/main.bicep:1300-1305`, five apps at - `:1079,1178,1305,1378,1475`; `MMCA.ADC/infra/main.bicep:1578-1582`, six apps at - `:1200,1324,1448,1600,1695,1813`). This is the ADR-025 warm-up gate doing rollout duty. + until the new one is warm (`MMCA.Store/infra/main.bicep:1332-1336`, five apps at + `:1097,1202,1335,1409,1507`; `MMCA.ADC/infra/main.bicep:1631-1639`, six apps at + `:1218,1347,1476,1633,1729,1848`). This is the ADR-025 warm-up gate doing rollout duty. - **A post-deploy smoke gate is the last step of the deploy job.** `Smoke test (rollback on failure)` probes the freshly deployed fleet from outside Azure (`MMCA.Store/.github/workflows/deploy.yml:1059`, `MMCA.ADC/.github/workflows/deploy.yml:1099`). The diff --git a/docs-src/adr/081-cost-baseline-deploy-gate.md b/docs-src/adr/081-cost-baseline-deploy-gate.md index 58bcb53..b01d3f4 100644 --- a/docs-src/adr/081-cost-baseline-deploy-gate.md +++ b/docs-src/adr/081-cost-baseline-deploy-gate.md @@ -6,9 +6,9 @@ Accepted (2026-08-14). ## Context Both deployed apps run a deliberately small production footprint: every Container App is declared with `maxReplicas: 2` and every SQL database with the `Basic` tier -(`MMCA.Store/infra/main.bicep:1083,1182,1309,1384,1481` and `:594-598,624-628`; -`MMCA.ADC/infra/main.bicep:1211,1335,1459,1708,1826` and `:634-638,667-671`, with ADC's Notification -app deliberately right-sized at `maxReplicas: 1`, `MMCA.ADC/infra/main.bicep:1614-1616`). That +(`MMCA.Store/infra/main.bicep:1101,1206,1339,1415,1513` and `:594-598,624-628`; +`MMCA.ADC/infra/main.bicep:1228,1357,1486,1741,1860` and `:634-638,667-671`, with ADC's Notification +app deliberately right-sized at `maxReplicas: 1`, `MMCA.ADC/infra/main.bicep:1646-1648`). That footprint is the cost baseline, and it is what the monthly bill is planned against. The footprint is also expected to move temporarily. A conference day, a load test, a slow query under @@ -18,10 +18,10 @@ it is silent: nothing breaks, no alert fires on a healthy oversized system, and traffic perfectly while costing several times its baseline. The existing control against that was the monthly Azure budget declared in both Bicep templates -(`MMCA.Store/infra/main.bicep:508-534`, `MMCA.ADC/infra/main.bicep:551`), which notifies at 80% of +(`MMCA.Store/infra/main.bicep:519-547`, `MMCA.ADC/infra/main.bicep:562`), which notifies at 80% of actual spend and 100% of forecast spend. The Store template names the exact case it is meant to catch in its own comment, "a scale-up (manual SQL-tier / replica) silently running for weeks" -(`MMCA.Store/infra/main.bicep:505-507`). A spend threshold is a lagging indicator: by the time it +(`MMCA.Store/infra/main.bicep:517`). A spend threshold is a lagging indicator: by the time it trips, weeks of the overspend have already happened, and the notification says a number, not which resource is wrong. What was missing was a check on the **configuration** itself, and a moment at which someone would have to look at it. @@ -63,7 +63,7 @@ The cost baseline is asserted by a **read-only reusable workflow** that both run (`MMCA.Store/.github/workflows/cost-guard.yml:82`), because its retained legacy `MMCAStore` archive was downgraded from S0 to Basic and the bump back to S0 is the documented rollback path, so a rollback must not read as drift (`MMCA.Store/.github/workflows/cost-guard.yml:9-12`, and the same - reasoning beside the resource at `MMCA.Store/infra/main.bicep:586-588`). + reasoning beside the resource at `MMCA.Store/infra/main.bicep:596-599`). - **It never mutates anything.** Every call is an `az ... list` or `az ... show`; the job's stated contract is that on drift it fails the run and prints what to reset diff --git a/docs-src/adr/082-two-tier-cors-posture.md b/docs-src/adr/082-two-tier-cors-posture.md index 4bfc0e6..ba802fa 100644 --- a/docs-src/adr/082-two-tier-cors-posture.md +++ b/docs-src/adr/082-two-tier-cors-posture.md @@ -29,23 +29,26 @@ Ship **two** cross-origin policies from the framework: an allow-listed one for s deliberately broader one for gateways. - **Service hosts register two named policies from one call.** `AddCommonCors(IConfiguration)` - (`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs:543`) - adds `_allowSpecificOrigins` (`:32`, `:547`) and `_allowAll` (`:35`, `:556`). Neither is the + (`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationBuilderExtensions.cs:579`) + adds `_allowSpecificOrigins` (`:34`, `:583`) and `_allowAll` (`:37`, `:592`). Neither is the default policy, so nothing applies until the pipeline names one. - **The service policy allow-lists origins, headers and methods, and allows credentials.** Origins - come from `Cors:AllowedOrigins` (`:549`), headers are the four the APIs actually use - (`Content-Type`, `Authorization`, `x-signalr-user-agent`, `x-requested-with`, `:551`), methods are - five explicit verbs (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `:552`), and `AllowCredentials()` - (`:553`) is what lets cookie and bearer traffic cross. The `x-signalr-user-agent` entry is + come from `Cors:AllowedOrigins` (`:585`), headers are the four the APIs actually use + (`Content-Type`, `Authorization`, `x-signalr-user-agent`, `x-requested-with`, `:587`), methods are + five explicit verbs (`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `:588`), and `AllowCredentials()` + (`:589`) is what lets cookie and bearer traffic cross. The `x-signalr-user-agent` entry is load-bearing rather than decorative: it plus credentials is what allows the SignalR hub to negotiate cross-origin with a bearer token (`MMCA.ADC/Source/Services/MMCA.ADC.Notification.Service/Program.cs:150-154`). - **The environment picks between the two policies in the shared middleware pipeline, not at - registration.** `UseCommonMiddlewarePipeline` calls `app.UseCors(...)` with `CorsPolicyAllowAll` - when `app.Environment.IsDevelopment()` and `CorsPolicyAllowSpecificOrigins` otherwise - (`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/WebApplicationExtensions.cs:93-95`), - positioned after `UseRouting()` (`:92`) and before `UseAuthentication()` (`:96`). Registration - stays environment-agnostic; one line decides the posture. + registration.** The selection is one named step of the ADR-079 pipeline: the `Cors` step calls + `app.UseCors(...)` with `CorsPolicyAllowAll` when `app.Environment.IsDevelopment()` and + `CorsPolicyAllowSpecificOrigins` otherwise + (`MMCA.Common/Source/Presentation/MMCA.Common.API/Startup/MiddlewarePipelineBuilder.cs:102-106`), + seeded by `CreateDefault()` (`:31`) after the `Routing` step (`:98-100`) and before + `Authentication` (`:108-110`). Hosts reach it through `UseCommonMiddlewarePipeline()` + (`WebApplicationExtensions.cs:46`, `:58`), which delegates to that builder (`:138`, `:140`). + Registration stays environment-agnostic; one step decides the posture. - **The gateway gets a different policy, and it is the default policy.** `AddCommonGatewayCors(IConfiguration, IHostEnvironment)` (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/GatewayCorsExtensions.cs:24`) lives in the Aspire @@ -59,7 +62,7 @@ deliberately broader one for gateways. - **Both tiers carry a Development-only allow-any-origin branch behind an S5122 suppression.** The service `_allowAll` policy is `AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()` under `#pragma warning disable S5122` whose comment names the pipeline line that gates it - (`WebApplicationBuilderExtensions.cs:555-560`); the gateway's Development branch is the same shape + (`WebApplicationBuilderExtensions.cs:591-596`); the gateway's Development branch is the same shape under the same suppression (`GatewayCorsExtensions.cs:34-41`), selected by `environment .IsDevelopment()` at registration time because the gateway has only one policy slot. - **Allowed origins are configuration, empty by default, filled at deploy time.** Every host ships @@ -70,7 +73,7 @@ deliberately broader one for gateways. `MMCA.Store/Source/Services/MMCA.Store.Sales.Service/appsettings.json:22`), and Bicep injects the real value into the gateway container of both applications as `Cors__AllowedOrigins__0`, pointing at the UI container app's FQDN - (`MMCA.ADC/infra/main.bicep:1665`, `MMCA.Store/infra/main.bicep:1363`). On Store the same key can + (`MMCA.ADC/infra/main.bicep:1698`, `MMCA.Store/infra/main.bicep:1394`). On Store the same key can also arrive from Key Vault as `Cors--AllowedOrigins--0`, which is why the vault provider is registered before anything reads configuration: the allow-list binds eagerly (`MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Program.cs:41-49`, `:50`). @@ -81,10 +84,10 @@ deliberately broader one for gateways. `MMCA.Store.Sales.Service/Program.cs:147`), as does the Helpdesk reference host (`MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/Program.cs:32`), and every one of the eight then runs `UseCommonMiddlewarePipeline()` so the selection above applies - (`MMCA.ADC.Identity.Service/Program.cs:322`, `MMCA.ADC.Conference.Service/Program.cs:392`, - `MMCA.ADC.Engagement.Service/Program.cs:328`, `MMCA.ADC.Notification.Service/Program.cs:258`, - `MMCA.Store.Catalog.Service/Program.cs:266`, `MMCA.Store.Identity.Service/Program.cs:257`, - `MMCA.Store.Sales.Service/Program.cs:275`, `MMCA.Helpdesk.Web/Program.cs:111`). Both gateways call + (`MMCA.ADC.Identity.Service/Program.cs:322`, `MMCA.ADC.Conference.Service/Program.cs:399`, + `MMCA.ADC.Engagement.Service/Program.cs:334`, `MMCA.ADC.Notification.Service/Program.cs:264`, + `MMCA.Store.Catalog.Service/Program.cs:268`, `MMCA.Store.Identity.Service/Program.cs:257`, + `MMCA.Store.Sales.Service/Program.cs:277`, `MMCA.Helpdesk.Web/Program.cs:117`). Both gateways call `AddCommonGatewayCors` and the bare `app.UseCors()` (`MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/Program.cs:82`, `:120`; `MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Program.cs:64`, `:143`). @@ -118,17 +121,17 @@ surface, so there is no third tier. allows every origin and gate it solely on `IHostEnvironment.IsDevelopment()`. Anything that boots one of these hosts with `ASPNETCORE_ENVIRONMENT=Development` on a reachable network gets the open policy, and the S5122 suppressions - (`WebApplicationBuilderExtensions.cs:555`, `GatewayCorsExtensions.cs:36`) mean the analyzer will + (`WebApplicationBuilderExtensions.cs:591`, `GatewayCorsExtensions.cs:36`) mean the analyzer will not say so again. The compensating control is ADR-058's `ProductionHostApplicationFactory`, which pins `UseEnvironment("Production")` so conformance runs exercise the restrictive branch. - **The service allow-list is a framework edit, not a host setting.** Headers and methods are - hardcoded in `AddCommonCors` (`:551-552`), so a service that needs a sixth verb or a fifth header + hardcoded in `AddCommonCors` (`:587-588`), so a service that needs a sixth verb or a fifth header needs an MMCA.Common change and a lockstep version bump (ADR-016), not an appsettings entry. That is the deliberate direction of the trade (precision over per-host configurability), but it does make the cheap change the expensive one. - **Origins are a deploy-time responsibility with no startup validation.** `Cors:AllowedOrigins` is read with a raw `configuration.GetSection(...).Get() ?? []` in both registrations - (`WebApplicationBuilderExtensions.cs:549`, `GatewayCorsExtensions.cs:45-47`); there is no options + (`WebApplicationBuilderExtensions.cs:585`, `GatewayCorsExtensions.cs:45-47`); there is no options class, no `ValidateOnStart`, and no entry in the fail-fast configuration contract (ADR-070). A host deployed without the value starts happily and fails closed at the first cross-origin request, which is the safe direction but shows up as a browser console error rather than a boot failure. diff --git a/docs-src/adr/083-crud-lifecycle-event-taxonomy.md b/docs-src/adr/083-crud-lifecycle-event-taxonomy.md index 522d849..68ef7ed 100644 --- a/docs-src/adr/083-crud-lifecycle-event-taxonomy.md +++ b/docs-src/adr/083-crud-lifecycle-event-taxonomy.md @@ -1,7 +1,8 @@ # ADR-083: One CRUD Lifecycle Event per Entity with a State Discriminator ## Status -Accepted (2026-08-14). +Accepted (2026-08-14). Revised 2026-08-23: the adopter counts were refreshed (ADC Conference's +`ActivityChanged` joined the base-derived set) and three source citations were re-anchored. ## Context ADR-003 decides how a domain event **moves**: captured into the outbox inside `SaveChangesAsync`, @@ -35,7 +36,7 @@ carrying a `DomainEntityState` discriminator; handlers filter on `State`. (`MMCA.Common/Source/Core/MMCA.Common.Domain/Enums/DomainEntityState.cs:7-13`). No production call site in any repo raises `Unchanged`: it is the zero default, and it appears only as a negative `[InlineData]` case in handler tests - (`MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/Points/DomainEventHandlers/SessionQuestionSubmittedPointsHandlerTests.cs:45,60`, + (`MMCA.ADC/Tests/Modules/Engagement/MMCA.ADC.Engagement.Application.Tests/Points/DomainEventHandlers/SessionQuestionSubmittedPointsHandlerTests.cs:79,190-191`, `MMCA.ADC/Tests/Modules/Conference/MMCA.ADC.Conference.Application.Tests/DomainEvents/SessionCreatedHandlerTests.cs:40`). - **`Added` from the factory, `Updated` from mutators, `Deleted` from `Delete()`.** The base's usage note fixes the mapping (`EntityChangedEvent.cs:10-13`), and `Session` is the canonical shape: one @@ -47,7 +48,7 @@ carrying a `DomainEntityState` discriminator; handlers filter on `State`. `SessionChanged` and returns immediately unless the state is `Added` (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Application/Sessions/DomainEventHandlers/SessionCreatedHandler.cs:17-18`), and the points award for asking a question does the same on `SessionQuestionChanged` - (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/Points/DomainEventHandlers/SessionQuestionSubmittedPointsHandler.cs:48-49`). + (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Application/Points/DomainEventHandlers/SessionQuestionSubmittedPointsHandler.cs:60-63`). A handler that genuinely wants every transition writes no filter and logs the discriminator instead: `TicketChangedAuditHandler` passes `domainEvent.State` straight into its `LoggerMessage` template (`MMCA.Helpdesk/Source/Modules/Tickets/MMCA.Helpdesk.Tickets.Application/Tickets/DomainEventHandlers/TicketChangedAuditHandler.cs:23,28-29`). @@ -73,14 +74,15 @@ carrying a `DomainEntityState` discriminator; handlers filter on `State`. (`MMCA.Store/Tests/Architecture/MMCA.Store.Architecture.Tests/IntegrationEventContractTests.cs:11`), so retyping or removing the discriminator fails the build and, under ADR-010, requires a new event type rather than a silent reshape. -- **The shared base is the convenience; the discriminator shape is the convention.** Thirteen concrete +- **The shared base is the convenience; the discriminator shape is the convention.** Fourteen concrete records derive `EntityChangedEvent` across the four repos: six in Store (`OrderChanged.cs:19`, `ShoppingCartChanged.cs:16`, `InventoryItemChanged.cs:17` under `Sales.Domain`, `Catalog.Domain/Products/DomainEvents/ProductChanged.cs:23`, `Catalog.Domain/Categories/DomainEvents/CategoryChanged.cs:19`, - `Identity.Domain/Customers/DomainEvents/CustomerChanged.cs:25`), six in ADC Conference + `Identity.Domain/Customers/DomainEvents/CustomerChanged.cs:25`), seven in ADC Conference (`SponsorChanged.cs:16`, `EventChanged.cs:16`, `QuestionChanged.cs:16`, `CategoryChanged.cs:16`, - `SpeakerChanged.cs:21`, `SessionChanged.cs:18`), and one in Helpdesk (`TicketChanged.cs:15`). + `ActivityChanged.cs:16`, `SpeakerChanged.cs:21`, `SessionChanged.cs:18`), and one in Helpdesk + (`TicketChanged.cs:15`). Nineteen further event types follow the same one-event-with-`State` shape while inheriting `BaseDomainEvent` or `BaseIntegrationEvent` directly, either because they identify a parent/child pair rather than a single entity (`RoomChanged` carries `EventId` + `RoomId`, @@ -88,9 +90,9 @@ carrying a `DomainEntityState` discriminator; handlers filter on `State`. `ShoppingCartItemChanged` carries `CustomerId` + `ProductVariantId`, `MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.Domain/ShoppingCarts/DomainEvents/ShoppingCartItemChanged.cs:16-22`) or because the module took the shape without the base (all seven ADC Engagement events, for example - `MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Domain/UserSessionBookmarks/DomainEvents/UserSessionBookmarkChanged.cs:15-20`, + `MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Domain/UserSessionBookmarks/DomainEvents/UserSessionBookmarkChanged.cs:21-26`, whose doc cites the same rule as BR-60 at `:8-9`). Counting the shape rather than the base type: - **22 in ADC, 9 in Store, 1 in Helpdesk**. + **23 in ADC** (16 Conference, 7 Engagement), **9 in Store, 1 in Helpdesk**. - **Nothing enforces the taxonomy.** The shared fitness rules require domain events to be sealed and to live in a `*.DomainEvents` namespace (`MMCA.Common/Source/Hosting/MMCA.Common.Testing.Architecture/ArchitectureRules.Naming.cs:52-60`), to @@ -115,7 +117,7 @@ records exactly that gap (`TicketChangedAuditHandler.cs:13-14`). Outside the fou `MMCA.ECommerce` companion sample carries two more adopters on the same pattern (`MMCA.ECommerce/Source/Modules/Products/MMCA.ECommerce.Products.Domain/Products/DomainEvents/ProductChanged.cs:15`, `.../Orders/MMCA.ECommerce.Orders.Domain/Orders/DomainEvents/OrderChanged.cs:15`), which brings the -total number of records deriving the base to **15**. +total number of records deriving the base to **16**. ## Rationale - **One type per entity is one subscription surface.** A subscriber declares interest in the entity, @@ -150,7 +152,7 @@ total number of records deriving the base to **15**. tell the transitions apart (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Domain/LivePolls/DomainEvents/LivePollChanged.cs:9-11,17-22`), which is a state machine expressed through the CRUD shape rather than as its own events. -- **The base type is optional in practice.** 13 of the 32 lifecycle events across the three apps derive +- **The base type is optional in practice.** 14 of the 33 lifecycle events across the three apps derive `EntityChangedEvent`; the other 19 re-declare the same two members on `BaseDomainEvent`. Consistency is a review convention, not a fitness function (ADR-015), so a new module can drift without a failing test. diff --git a/docs-src/adr/084-stripe-webhook-ingress.md b/docs-src/adr/084-stripe-webhook-ingress.md index fdc73a0..bb5cb79 100644 --- a/docs-src/adr/084-stripe-webhook-ingress.md +++ b/docs-src/adr/084-stripe-webhook-ingress.md @@ -42,7 +42,9 @@ and **a self-registering, self-provisioning endpoint registration at startup**. deliberately leaves `Version` and `VersionPolicy` unset, unlike `catalog` and `identity` which pin `Version` 2 (`appsettings.json:83-90`, reasoning at `MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Program.cs:85-89`). That is why Sales keeps the ADR-012 - mixed-endpoint profile (`MMCA.Store/infra/main.bicep:1207`). + mixed-endpoint profile: its container app's ingress stays `transport: 'http'` for REST plus this + webhook, with a TCP-passthrough `additionalPortMappings` entry carrying the h2c gRPC port alongside + it (`MMCA.Store/infra/main.bicep:1231-1246`). - **The status code encodes ACCEPTED, not PROCESSED.** Exactly three error codes return 400, held in a `FrozenSet` named `RejectionCodes`: `SignatureVerificationFailed`, `ParseFailed` and `SecretMissing` (`PaymentsController.cs:31-36`, defined at @@ -76,7 +78,7 @@ and **a self-registering, self-provisioning endpoint registration at startup**. skips entirely when `WebhookBaseUrl` is empty, which is the local-development path where the Stripe CLI forwards instead (`:54-58`), and skips with a `Warning` when `SecretKey` is empty (`:60-64`). The expected URL is `WebhookBaseUrl` plus the constant `/Payments/webhook` (`:33,84`); production - injects `Stripe__WebhookBaseUrl` as the Gateway FQDN (`main.bicep:1296`), so the registered endpoint + injects `Stripe__WebhookBaseUrl` as the Gateway FQDN (`main.bicep:1326`), so the registered endpoint is the Gateway route above. It subscribes exactly three event types (`:42-47`), and warns when an existing endpoint is missing any of them (`:224-234`). - **It deletes only endpoints it created itself.** Every auto-created endpoint carries the description diff --git a/docs-src/adr/085-identifier-type-aliases-revisited.md b/docs-src/adr/085-identifier-type-aliases-revisited.md index a95aab6..846ddd7 100644 --- a/docs-src/adr/085-identifier-type-aliases-revisited.md +++ b/docs-src/adr/085-identifier-type-aliases-revisited.md @@ -1,7 +1,10 @@ # ADR-085: Identifier Type Aliases Revisited (Wrapper Structs Deferred Again, With Triggers) ## Status -Accepted (2026-08-18). **Revisits [ADR-048](048-primitive-identifier-type-aliases.md)**, which stays +Accepted (2026-08-18). Revised 2026-08-23 (the alias count and the migration-surface census were +recounted, the census gained a stated methodology, and the `CheckIn` and generic-parameter citations +were corrected; see the Revision (2026-08-23) at the end). +**Revisits [ADR-048](048-primitive-identifier-type-aliases.md)**, which stays Accepted and unchanged in substance: the aliases remain the identifier model. What changes is the shape of the deferral. ADR-048 left the wrapper-struct alternative "considered and left unbuilt" with no condition attached; this record measures what the deferral actually costs today, states the @@ -22,16 +25,19 @@ at once (specification-first reads, keyset pagination, projection pushdown; see going to ride along with unrelated churn, that was the wave to fold it into. It did not, and this record says why. -Three facts frame the decision, all counted in the four repositories' `Source` trees on 2026-08-18: - -- **43 aliases live in 10 files across the four repos.** MMCA.Common declares 3 (`UserIdentifierType` - in `Source/Core/MMCA.Common.Domain/GlobalUsings.IdentifierType.cs` plus the two push-notification - aliases in `Source/Core/MMCA.Common.Shared/GlobalUsings.NotificationIdentifierType.cs`); MMCA.ADC - declares 29 across Conference (16), Engagement (10), Identity (1) and Notification (2); MMCA.Store - declares 9 across Catalog (4), Sales (3) and Identity (2); MMCA.Helpdesk declares 2 in Tickets. -- **42 of the 43 resolve to `int`.** The single exception is ADC's - `SpeakerIdentifierType = System.Guid`, which Sessionize forces. So for every practical purpose the - whole workspace has **one** identifier CLR type, and the compiler sees 42 synonyms for it. +Three facts frame the decision, all counted in the four repositories' `Source` trees on 2026-08-23: + +- **44 aliases live in 10 files across the four repos.** MMCA.Common declares 3 (`UserIdentifierType` + in `Source/Core/MMCA.Common.Domain/GlobalUsings.IdentifierType.cs:1` plus the two push-notification + aliases in `Source/Core/MMCA.Common.Shared/GlobalUsings.NotificationIdentifierType.cs:1-2`); + MMCA.ADC declares 30 across Conference (17, alias file `:5-21`), Engagement (10, `:4-13`), + Identity (1, `:2`) and Notification (2, `:1-2`); MMCA.Store declares 9 across Catalog (4), Sales (3) + and Identity (2); MMCA.Helpdesk declares 2 in Tickets. +- **43 of the 44 resolve to `int`.** The single exception is ADC's + `SpeakerIdentifierType = System.Guid` + (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:19`), + which Sessionize forces. So for every practical purpose the whole workspace has **one** identifier + CLR type, and the compiler sees 43 synonyms for it. - **No wrapper-struct identifier type and no generator package exists anywhere.** A content sweep of the four `Source` trees for `Vogen` and `StronglyTypedId` returns nothing: not a package reference, not a project file entry, not a using. ADR-048's "considered and left unbuilt" is still literally @@ -50,11 +56,16 @@ foreign keys, so the type system is the only check there is and the type system The clearest live instance is ADC's `CheckIn` aggregate (`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Domain/CheckIns/CheckIn.cs:57-64`), whose -constructor takes five identifiers in a row: `UserIdentifierType userId`, `EventIdentifierType +constructor takes five identifiers, four of them consecutive: `UserIdentifierType userId` (`:58`) is +held apart from the run by the `CheckInScope scope` parameter (`:59`), and then `EventIdentifierType eventId`, `SessionIdentifierType? sessionId`, `SponsorIdentifierType? sponsorId` and -`UserIdentifierType checkedInByUserId`, mirrored on the `Create` factory (`:89-91`). Four of the five -are `int` or `int?` at the CLR level, and two of them are the *same* alias holding two different -users (the attendee and the organizer who scanned the badge). Swapping those two arguments compiles +`UserIdentifierType checkedInByUserId` follow one another (`:60-63`). The `Create` factory carries the +same five identifiers across a wider signature (`:89-96`) rather than mirroring the order: `sponsorId` +moves to last and optional (`:96`) so the scan and manual paths, which can never carry one, stay +unchanged. All five are `int` or `int?` at the CLR level, since the four aliases they draw on +(`UserIdentifierType`, `EventIdentifierType`, `SessionIdentifierType`, `SponsorIdentifierType`) are +every one of them `int`, and two of the five are the *same* alias holding two different users (the +attendee and the organizer who scanned the badge). Swapping those two arguments compiles cleanly, passes every type check, and produces a check-in attributed to the wrong person. A wrapper struct would have made that line a compiler error. This is the concrete cost, stated once with a real example rather than as an abstraction. @@ -67,15 +78,24 @@ implement) so the boilerplate is not hand-written, plus an EF Core `ValueConvert the objection is not that the wrappers are laborious to author. The objection is the blast radius of switching. -That radius is measurable. The alias token appears **3,641 times across 1,016 files** in the four -`Source` trees alone, tests excluded: 712 occurrences in 126 files in MMCA.Common, 1,996 in 582 files -in MMCA.ADC, 879 in 278 files in MMCA.Store, and 54 in 30 files in MMCA.Helpdesk. Every one of those -is a signature, a property, a generic argument, or a DTO field that a wrapper migration would have to -either change or prove it can leave alone. Because MMCA.Common is a published package family released -in lockstep ([ADR-016](016-lockstep-versioning-masstransit-pin.md)), the framework half of that count +That radius is measurable, and the measurement only means something with its counting rule stated. +An **alias token** here is any `*IdentifierType` token other than the framework's own generic +parameter `TIdentifierType`, counted in the `.cs` and `.razor` files of the four `Source` trees on +2026-08-23, with tests, `bin` and `obj` excluded. On that rule the aliases appear **3,192 times across +1,001 files**: 187 occurrences in 80 files in MMCA.Common, 2,077 in 613 files in MMCA.ADC, 873 in 277 +files in MMCA.Store, and 55 in 31 files in MMCA.Helpdesk. Excluding `TIdentifierType` is what makes +the framework figure honest: 538 of MMCA.Common's 725 `IdentifierType` hits are that generic +parameter, which a wrapper migration re-satisfies with a new type argument rather than rewrites call +site by call site. Every one of the 3,192 is a signature, a property, a generic argument, or a DTO +field that a wrapper migration would have to either change or prove it can leave alone. Because +MMCA.Common is a published package family released in lockstep +([ADR-016](016-lockstep-versioning-masstransit-pin.md)), the framework share of that count is a breaking public-API change that all three consumers must absorb in a single sweep, and the -identifier type is a generic parameter on -`BaseEntity`, `IBaseDTO`, `IEntityDTOMapper` +identifier type is a generic parameter on `BaseEntity` +(`MMCA.Common/Source/Core/MMCA.Common.Domain/Entities/BaseEntity.cs:14`), `IBaseDTO` +(`MMCA.Common/Source/Core/MMCA.Common.Shared/DTOs/IBaseDTO.cs:9`) and +`IEntityDTOMapper` +(`MMCA.Common/Source/Core/MMCA.Common.Application/Interfaces/IEntityDTOMapper.cs:14`) ([ADR-001](001-manual-dto-mapping.md)), the repository handles ([ADR-055](055-repository-and-specification-contract.md)) and the generic entity query surface ([ADR-034](034-generic-entity-query-layer.md)), so the change is not confined to leaf code. @@ -102,7 +122,7 @@ Absent all three, this stays a recorded, priced deferral rather than an open que - **The cost is paid once and the benefit accrues per defect avoided, and the defect count is currently zero.** No production incident in any of the four repos has been traced to a swapped identifier. That is not proof of safety, and this record does not claim it is; it is the only - evidence available, and it does not support a 1,016-file change. + evidence available, and it does not support a 1,001-file change. - **A partial migration is worse than either endpoint.** Wrapping some identifiers and not others produces a codebase where the absence of a compiler error means nothing, because the reader cannot tell whether a given call site is protected or merely un-migrated. The change is therefore @@ -129,14 +149,15 @@ Absent all three, this stays a recorded, priced deferral rather than an open que naming convention that a reviewer could mechanically check. Trigger 1 therefore depends on a production defect being *traced* to a transposition, and a wrong-user check-in is exactly the kind of defect that gets written off as a scanning mistake instead. -- **The migration price rises with the codebase.** The 3,641 occurrences counted here are a snapshot +- **The migration price rises with the codebase.** The 3,192 occurrences counted here are a snapshot and the number only grows. Deferring on cost grounds means the cost argument gets stronger every release, which is the classic shape of a decision that is never revisited on its merits. - **Trigger 3 is not measured.** No count of cross-module scalar identifier references is maintained, so "keeps climbing" has no baseline to climb from. It is a qualitative trigger and is recorded as such. -- **Ordering conventions carry weight the type system should.** With four `int` parameters in a row, - the discipline that keeps `CheckIn.Create` correct is parameter naming and the doc comments at +- **Ordering conventions carry weight the type system should.** With three `int` parameters in a row + in `Create` (`CheckIn.cs:92-94`) and four in the constructor (`:60-63`), the discipline that keeps + `CheckIn.Create` correct is parameter naming and the seven parameter doc lines at `CheckIn.cs:81-87`. That is review-strength protection standing in for compile-time protection, which is the same class of dependency ADR-048 already recorded for the alias convention itself. @@ -153,3 +174,41 @@ alias convention nor identifier transposition), [ADR-055](055-repository-and-specification-contract.md) and [ADR-034](034-generic-entity-query-layer.md) (the generic surfaces parameterized by the identifier type, and therefore in the migration's blast radius). + +## Revision (2026-08-23) +The decision, the priced alternative, the three triggers and the trade-offs are unchanged. What +changed is arithmetic and three citations. + +**The alias count is 44 across 10 files, 43 of them `int`.** ADC's Conference module gained +`ActivityIdentifierType = int` at the head of its alias file +(`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.Shared/MMCA.ADC.Conference.GlobalUsings.IdentifierType.cs:5-21`), +taking Conference to seventeen and ADC to thirty. `SpeakerIdentifierType = System.Guid` (`:19` in the +same file) is still the only non-`int` alias in any of the four repositories. +[ADR-048](048-primitive-identifier-type-aliases.md) carries the same pair. + +**The migration-surface census is restated with its counting rule and re-measured.** The previous +figure (3,641 occurrences across 1,016 files) counted the framework's generic parameter +`TIdentifierType` as if it were an alias, which inflated MMCA.Common's share roughly fourfold: of the +725 `IdentifierType` hits in `MMCA.Common/Source`, 538 are the generic parameter and only 187 are +alias tokens. Counting alias tokens only, in `.cs` and `.razor` under the four `Source` trees with +tests, `bin` and `obj` excluded, gives **3,192 occurrences across 1,001 files** (Common 187/80, +ADC 2,077/613, Store 873/277, Helpdesk 55/31). The conclusion is unmoved: the blast radius is still +a four-repository, thousand-file change, and MMCA.Common's generic parameters are still in it, since +re-satisfying a type parameter with a wrapper struct is a breaking public-API change even where no +call site is edited. + +**Three citations corrected.** ADC's `CheckIn` constructor takes five identifiers but not five in a +row: `CheckInScope scope` (`CheckIn.cs:59`) sits between `userId` (`:58`) and the consecutive run of +four (`:60-63`). The `Create` factory is not a mirror of the constructor: its signature spans +`:89-96` and moves `sponsorId` to last and optional (`:96`). And all five of those identifier +parameters are `int` or `int?` at the CLR level, not four of the five, which sharpens rather than +softens this record's point: nothing in the CLR distinguishes any one of them from the others. +Finally, the two framework generic surfaces are spelled `IBaseDTO` +(`IBaseDTO.cs:9`) and `IEntityDTOMapper` +(`IEntityDTOMapper.cs:14`); this record previously named parameters (`TId`, `TDTO`) that do not +exist. + +The Rationale's "no production incident traced to a swapped identifier" stands as written and stays +unverified by design: the four repositories keep no incident register, postmortem folder or issue +label that would record such a defect, which is exactly why the record already refuses to call it +proof of safety. diff --git a/docs-src/adr/087-broker-poison-message-handling.md b/docs-src/adr/087-broker-poison-message-handling.md index 5324eec..bee33a1 100644 --- a/docs-src/adr/087-broker-poison-message-handling.md +++ b/docs-src/adr/087-broker-poison-message-handling.md @@ -53,26 +53,26 @@ minute, ten minutes, one hour. Both live in the `"MessageBus"` section (`:14`). The two transports consume them differently, and the asymmetry is the decision: - **RabbitMQ consults the flag.** `ConfigureBrokerTransport` - (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:794`) calls - `cfg.UseDelayedRedelivery(r => r.Intervals(intervals))` inside `UsingRabbitMq` (`:802`) only under - `if (settings.EnableDelayedRedelivery)` (`:813`, the call at `:818`), with the plugin requirement - restated at the registration site (`:783-787`, `:809-812`). Default-off is not timidity: the local + (`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/DependencyInjection.cs:807`) calls + `cfg.UseDelayedRedelivery(r => r.Intervals(intervals))` inside `UsingRabbitMq` (`:815`) only under + `if (settings.EnableDelayedRedelivery)` (`:826`, the call at `:831`), with the plugin requirement + restated at the registration site (`:794-801`, `:822-825`). Default-off is not timidity: the local Aspire broker cannot serve it, so a default-on setting would break every developer's first `F5` with a bus-start failure, which is the worst possible place to learn about a broker plugin. -- **Azure Service Bus does not consult it.** `UsingAzureServiceBus` (`:832`) calls - `UseDelayedRedelivery` unconditionally (`:846`), with the reasoning recorded inline (`:839-842`). +- **Azure Service Bus does not consult it.** `UsingAzureServiceBus` (`:845`) calls + `UseDelayedRedelivery` unconditionally (`:859`), with the reasoning recorded inline (`:852-856`). Service Bus schedules natively, there is no plugin to be missing, and a production transport that can express "try again in an hour" should always express it. Making the operator opt in would mean the environment that most needs the behavior is the one most likely to be running without it. Two details are worth stating so the words above are not read as stronger than the code. "Unconditional" means "not gated on the flag": both call sites are still guarded by -`intervals.Length > 0` (`:816`, `:844`), so an operator who configures an empty interval list turns +`intervals.Length > 0` (`:829`, `:857`), so an operator who configures an empty interval list turns the feature off everywhere. And `RedeliveryIntervalsSeconds` carries **no** DataAnnotations attribute, unlike its neighbours `RetryLimit` and the two retry-interval settings, so the ADR-070 fail-fast chain does not validate it; non-positive entries are filtered at use time in `BuildRedeliveryIntervals` -(`:873-876`) instead. In both transports the redelivery filter is registered **before** -`UseMessageRetry` (`:822`, `:849`), which is what keeps immediate retry innermost and delayed +(`:886-889`) instead. In both transports the redelivery filter is registered **before** +`UseMessageRetry` (`:835`, `:862`), which is what keeps immediate retry innermost and delayed redelivery outside it. ### A fault consumer makes an exhausted message visible @@ -186,7 +186,7 @@ database resilience posture**, recorded here so the asymmetry is a decision rath likely to run without second-level redelivery is the one that is not Azure Service Bus. Nothing warns a RabbitMQ host that the feature it never enabled is not running. - **The intervals are not validated at startup.** `RedeliveryIntervalsSeconds` sits outside the - ADR-070 fail-fast chain, so a typo becomes a filtered-out entry at `:873-876` rather than a refusal + ADR-070 fail-fast chain, so a typo becomes a filtered-out entry at `:886-889` rather than a refusal to boot. An operator who writes `[0, 0, 0]` silently gets no delayed redelivery at all. - **An hour-long redelivery window widens the duplicate window with it.** A message redelivered at `+3600s` runs its handlers an hour after the original attempt, so ADR-021's inbox and every diff --git a/docs-src/adr/088-gateway-edge-responsibilities.md b/docs-src/adr/088-gateway-edge-responsibilities.md index c353361..d450ebd 100644 --- a/docs-src/adr/088-gateway-edge-responsibilities.md +++ b/docs-src/adr/088-gateway-edge-responsibilities.md @@ -116,7 +116,7 @@ by the `"/Payments"` prefix at to Stripe is a retry and eventually a disabled endpoint, which silently stops every payment update ([ADR-084](084-stripe-webhook-ingress.md)); and ADC's SignalR hub route (`/hubs/{**catch-all}`, bypassed by the `"/hubs"` prefix at -`MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/appsettings.json:18`, route at `:122-125`), because a +`MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/appsettings.json:18`, route at `:126-129`), because a negotiate-plus-reconnect storm from one office's shared address is exactly the pattern a per-IP window misreads as abuse ([ADR-039](039-live-channel-push.md)). diff --git a/docs-src/adr/089-gateway-topology-owned-by-configuration.md b/docs-src/adr/089-gateway-topology-owned-by-configuration.md index 5a7ab78..c38dbf4 100644 --- a/docs-src/adr/089-gateway-topology-owned-by-configuration.md +++ b/docs-src/adr/089-gateway-topology-owned-by-configuration.md @@ -1,7 +1,9 @@ # ADR-089: Gateway Topology Owned by Configuration ## Status -Accepted (2026-08-18). **Amends [ADR-008](008-service-extraction-topology.md)**: that record gave the +Accepted (2026-08-18; revised 2026-08-23: ADC's table gained a 27th route, `/Activities`, on +2026-08-19, and the bicep anchors below are corrected). **Amends +[ADR-008](008-service-extraction-topology.md)**: that record gave the Gateway the route-to-service map and expressed it as code (a list of `MapForwarder` calls). The map itself is unchanged; what changed is where it is written. YARP `ReverseProxy` **configuration** is now the single source of the route table, the per-route HTTP version policy lives in cluster @@ -46,7 +48,7 @@ because it is the duplication a reader assumes exists. The Aspire AppHost holds start-ordering (`MMCA.ADC/Source/Hosting/MMCA.ADC.AppHost/Program.cs:252-262`, `MMCA.Store/Source/Hosting/MMCA.Store.AppHost/Program.cs:218-226`) and the bicep templates hold `services____http__0` environment variables on the gateway container app -(`MMCA.ADC/infra/main.bicep:1671-1674`, `MMCA.Store/infra/main.bicep:1369-1371`). Both are **address +(`MMCA.ADC/infra/main.bicep:1704-1707`, `MMCA.Store/infra/main.bicep:1400-1402`). Both are **address books**: service name to URL, with no path prefix anywhere in them. They answer "where does `conference` resolve" and never "what reaches conference", so neither is a second route table and neither should become one. @@ -80,8 +82,9 @@ Each gateway calls `AddReverseProxy().LoadFromConfig(builder.Configuration.GetSe and `MapReverseProxy()`, and the `MapForwarder` lists are deleted. ADC wires it at `MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/Program.cs:90-93` and maps it at `:138`; Store at `MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Program.cs:115-117` and `:150`. Routes and clusters live -in the gateway's own `appsettings.json`: 26 routes over five clusters for ADC -(`MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/appsettings.json:20-175`) and 10 routes over three clusters +in the gateway's own `appsettings.json`: 27 routes over five clusters for ADC (4 identity, 16 +conference, 5 engagement, 2 notification, at +`MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/appsettings.json:20-179`) and 10 routes over three clusters for Store (`MMCA.Store/Source/Hosts/MMCA.Store.Gateway/appsettings.json:19-92`). Destinations stay Aspire service-discovery names, so nothing about ADR-008's transport-at-the-edge posture, the AppHost wiring or the bicep address book changes: what changed is that the path-prefix-to-cluster mapping is @@ -194,7 +197,7 @@ declarative shape makes the difference a diff instead of an archaeology exercise `appsettings.json` has no `ForwardHttp2` key. Configuration makes divergence easier to see and does not make it impossible, and no shared framework code enforces the shape, since the gateways are consumer-owned hosts by design. -- **A JSON table reviews less well than a code diff.** A reviewer reading 26 routes in +- **A JSON table reviews less well than a code diff.** A reviewer reading 27 routes in `appsettings.json` has no types, no navigation and no compiler; the gain in editability is partly a loss in review signal, offset only by the test. diff --git a/docs-src/adr/092-web-vitals-budget-gate.md b/docs-src/adr/092-web-vitals-budget-gate.md new file mode 100644 index 0000000..55051d5 --- /dev/null +++ b/docs-src/adr/092-web-vitals-budget-gate.md @@ -0,0 +1,179 @@ +# ADR-092: Core Web Vitals Budget as a Shipped Test Contract and Deploy Gate + +## Status +Accepted (2026-08-23). + +## Context +Rubric section 23 asks for client-side performance that is measured rather than assumed, naming Core +Web Vitals (LCP, INP, CLS) or an equivalent as the evidence +(`Website/docs-src/governance/ArchitectureEvaluationCriteria.md:606`, category at `:596`). It is the +client-side complement of section 12 (`:349`), which the backend already answers two ways: ADR-060 +gates MMCA.Common's hot paths on a committed BenchmarkDotNet baseline, and the deployed apps run a k6 +load test against read endpoints on a schedule. + +Neither reaches the browser. A benchmark measures managed allocations and a ratio between two methods +in one process; k6 measures what the server returns, not when a page paints, whether it shifts under +the reader, or how long the first interaction takes to be handled. A Blazor render-mode change, a +heavier prerender payload, an unsized image, or a chrome change that reflows after hydration are all +invisible to both, and all of them are what the reader experiences as "slow". + +The measurement problem is the mirror image of ADR-060's. There, absolute wall-clock latency on a +shared runner was too noisy to assert. Here the numbers are noisy too (a two-core hosted runner +carrying SQL Server, Redis, RabbitMQ, every service, the UI and Playwright at once), but the metric +has something a microbenchmark does not: an externally defined "good" band that is orders of +magnitude above the measured values on this hardware. That gap is what makes an absolute client-side +ceiling assertable where an absolute nanosecond count is not. + +The third problem is placement. A number captured in a report nobody reads is not a budget. To be one +it has to fail something, and the only place a real engine already runs against a real stack is the +Playwright suite that ADR-063 made a deploy gate for accessibility. + +## Decision +Ship the measurement infrastructure and the assert mechanics in `MMCA.Common.Testing.E2E`, default +the budget to the Core Web Vitals good band, and let the assertions ride the existing deploy-gating +E2E suite. + +- **The collector is a shipped, dependency-free measurement type.** `WebVitalsCollector` + (`MMCA.Common/Source/Hosting/MMCA.Common.Testing.E2E/Infrastructure/WebVitalsCollector.cs:20`, in + the published package `MMCA.Common.Testing.E2E`, `MMCA.Common/FACTS.md:35`) installs + `PerformanceObserver` hooks as a Playwright init script so they exist before first paint + (`InstallAsync`, `:40`, script at `:26-35`), accumulating LCP, CLS, FCP and an event-timing INP + sample into `window.__vitals`. `CollectAsync` (`:47`) reads them back and stamps TTFB from + Navigation Timing (`:52-54`). No third-party JS and no network egress: every observer is wrapped in + `try/catch` so an engine lacking the entry type leaves that metric at 0 rather than throwing + (`:22-25`). +- **The budget is a record whose defaults are the good band.** `WebVitalsBudget` (`:103`) defaults to + `Lcp = 2500`, `Fcp = 1800`, `Ttfb = 800`, `Cls = 0.1` and `Inp = 500` (`:104-108`): the Core Web + Vitals good thresholds for LCP, FCP and CLS, plus a TTFB ceiling and a single-interaction INP + sample ceiling. The parameters are defaulted, not constants, so a consumer whose measured maxima + justify tighter numbers passes its own (`:92-96`). +- **A breach is a thrown test failure naming the page.** `AssertWithinBudget` (`:137`) asserts each + metric with `Should().BeLessThanOrEqualTo` (`:143-146`) and formats every failure as + `{metric} {measured} exceeded budget {budget} on {path}` (`:154-157`), so a red gate points at the + page rather than at a dashboard. +- **A zero INP is skipped, not read as a pass.** The INP assertion runs only when a sample was + actually recorded (`:148-151`): no interaction clearing the observer's 16 ms `durationThreshold` + (`:35`) leaves the field at 0, which must mean neither pass-by-absence nor failure. +- **Every run leaves a citable artifact and a citable line.** `WriteArtifactAsync` (`:63`) writes + `web-vitals-{label}.json` (`:70`) as a `WebVitalsArtifact` envelope (`:90`) wrapping the measured + `WebVitalsSample` (`:76`), under `WEB_VITALS_OUTPUT_DIR` or `artifacts/` beneath the working + directory (`:65-66`); `Describe` (`:118`) renders the same sample as one invariant-culture line + (`:122-124`) that `AssertWithinBudget` writes to test output. +- **Both deployed apps assert the shipped defaults.** ADC's `WebVitalsTests` + (`MMCA.ADC/Tests/E2E/MMCA.ADC.E2E.Tests/Workflows/WebVitalsTests.cs:23`) holds one shared + `WebVitalsBudget` constructed with no arguments (`:27`, stated at `:25-26`) and measures four + surfaces: home (`:36`), the public events entry point including its redirect (`:47`), the session + list (`:56`) and login (`:60`). Store's + (`MMCA.Store/Tests/E2E/MMCA.Store.E2E.Tests/Workflows/WebVitalsTests.cs:25`) constructs the same + defaults inline (`:68`, `:94`) over home (`:34`), the + catalog browse page (`:38`), login (`:42`) and product detail reached by navigation rather than a + hard-coded id (`:53`). Both drive one scripted search interaction on the grid page so the + event-timing observer records an INP sample (ADC `:70-81`, Store `:78-89`). +- **The numbers are calibrated against measured maxima, not picked to be safe.** ADC's remarks record + LCP 624 / FCP 444 / TTFB 27 ms / CLS 0.005 / INP 32 on run 29146540154, roughly 4x to 30x headroom + (`MMCA.ADC/.../WebVitalsTests.cs:14-17`); Store's record LCP 172 / FCP 172 / TTFB 26 ms / CLS 0 / + INP 24 on run 29146556386, roughly 10x to 30x + (`MMCA.Store/.../WebVitalsTests.cs:14-19`). Both were calibrated 2026-07-11. +- **The assertions ride the deploy gate because the workflow runs the whole project.** `e2e.yml` runs + `dotnet test --project ...E2E.Tests.csproj` with no filter (ADC `.github/workflows/e2e.yml:326-329`, + Store `:369-372`) and points `WEB_VITALS_OUTPUT_DIR` at the uploaded diagnostics directory (ADC + `:304`, Store `:364`). `deploy.yml` calls that workflow chromium-only as `e2e-gate` (ADC + `deploy.yml:531-542`, Store `:537-548`), and the `deploy` job both lists it in `needs` (ADC `:866`, + Store `:862`) and requires it to be `success` or `skipped` (ADC `:896`, Store `:893`). A front-end + performance budget is therefore a production precondition on the same footing as the SBOM, the + cost guard and the freshness gates, and `deploy.yml` says so where the k6 gate is defined (ADC + `:604-605`, Store `:611-612`). +- **The framework measures its own UI, under its own looser numbers.** `WebVitalsE2ETests` + (`MMCA.Common/Tests/Presentation/MMCA.Common.UI.E2E.Tests/WebVitalsE2ETests.cs:16`) uses the same + collector against the backend-less in-process gallery for the login and components pages + (`:28`, `:32`) but asserts three local constants, LCP 8000 ms, TTFB 4000 ms and CLS 0.25 + (`:18-20`, asserted at `:43-45`), rather than `WebVitalsBudget`: the gallery is a backstop against + catastrophic regression in the shared chrome (a render loop, a giant synchronous asset, a + layout-shifting theme change), not a calibrated app budget (`:8-14`). +- **The regression behaviour is pinned by unit tests, not by the browser runs.** + `WebVitalsBudgetTests` (`.../MMCA.Common.UI.E2E.Tests/WebVitalsBudgetTests.cs:12`) starts no + browser and covers exactly what only ever fires on a regression: that the defaults are the good + band (`:17-26`), that each of the five metrics fails when it exceeds its ceiling (`:45-56`), that a + 0 INP is skipped (`:58-64`), that a caller-supplied budget is honoured in both directions + (`:66-73`), and the exact text of the sample line (`:32-43`). + +Adoption is the two deployed apps plus the framework gallery. **MMCA.Helpdesk has neither**: it pins +the package (`MMCA.Helpdesk/Directory.Packages.props:66`) but has no E2E test project at all, so the +seed carries no worked example of a client-side budget, the same gap ADR-063 records for +accessibility. + +## Rationale +- **The good band is an external contract, which is what makes an absolute ceiling defensible here.** + ADR-060 refused absolute latency because a nanosecond count is a property of the runner. LCP 2500 ms + is not a claim about this runner: it is the published threshold the metric is defined against, and + the measured values sit 4x to 30x below it. The noise floor of a two-core hosted runner fits inside + that gap with room to spare, so the assertion survives a noisy neighbour and still fires on a real + regression. +- **Calibrating against observed maxima is what separates a budget from a backstop.** Numbers chosen + by feel end up either flaky or vacuous. Recording the run id and the measured maximum next to the + ceiling (ADC `WebVitalsTests.cs:14-17`) makes the headroom a reviewable fact and makes a future + tightening an evidence-based edit rather than a guess. +- **Ship the mechanics, keep the numbers with the consumer.** The assert body, the message format, + the artifact shape and the INP-zero carve-out are subtle and identical everywhere, so they belong in + the package (`WebVitalsCollector.cs:16-18`). The ceilings depend on hosting and runner, so they + belong to the app. Defaulted record parameters express exactly that split: both apps happen to take + the defaults today, and either can tighten without a framework change. +- **Reuse the gate that already exists.** A separate performance workflow would be another + twenty-minute Aspire boot and another thing to keep green. Because the suite runs the whole project, + adding a test class added a deploy-gating budget at zero marginal CI cost, the same lever ADR-063 + used for accessibility. +- **A single-interaction sample is honest about what it is.** Field INP is a p75 over real sessions + and cannot be produced by one scripted click. Naming the metric `INP-sample` in the output line + (`WebVitalsCollector.cs:124`) and in the parameter documentation (`:102`) keeps the assertion from + claiming more than it measured. +- **Artifacts make a red gate diagnosable and a green one auditable.** The JSON envelope plus the + one-line record mean a reviewer can compare today's numbers against the calibration run instead of + re-running the suite to find out what happened. + +## Trade-offs +- **The gate is ui-scoped and may legitimately skip.** Both apps gate `e2e-gate` on a `ui` change + filter (ADC `deploy.yml:538`, Store `:544`) and `deploy` accepts `skipped` for it (ADC `:896`, + Store `:893`), so a backend-only or infra-only deploy ships with no Web Vitals measurement of that + commit. Same intended cost trade as the accessibility gate, and the same caveat: "deployed" does not + always mean "the budget ran on this commit". +- **It never runs on a pull request.** The E2E project is in neither solution filter and the gate is + push/dispatch only (ADC `deploy.yml:538`, Store `:544`), so a regression is caught between merge and + rollout, not before merge. +- **The measured configuration is not the production one.** CI pins the UI to `InteractiveServer` + (ADC `e2e.yml:218`, Store `:210`), so the numbers describe Server-mode prerender-then-hydrate under + runner contention, not production's `InteractiveAuto` on real hardware (ADC + `WebVitalsTests.cs:10-13`, the caveat ADR-056 also records). +- **A marginal breach can be retried away.** The suite runs with `--retry-failed-tests 2` (ADC + `e2e.yml:329`, Store `:372`), which is what absorbs a contention spike but also means a budget that + fails once and passes twice reports green. +- **LCP and CLS are Chromium-only.** On Firefox and WebKit those observers fail silently and the + fields stay 0, so the assertions pass vacuously (`WebVitalsCollector.cs:14-16`). The deploy gate is + chromium-only, so this is real only for MMCA.Common's three-engine `ui-e2e` matrix + (`MMCA.Common/.github/workflows/ci.yml:237`), where two of the three legs assert LCP and CLS against + nothing. +- **Coverage is a hand-picked page list.** Four pages in ADC and four in Store, against far larger + inventories. Nothing forces a new page to acquire a budget, so breadth grows by discipline, the same + caveat as the accessibility suites. +- **Only pages with a search box get an INP sample.** The interaction is best-effort and page-specific + (ADC `WebVitalsTests.cs:73-78`, Store `:81-86`); on every other measured page INP stays 0 and its + assertion is skipped, so interaction latency is asserted on one page per app. +- **The green-run artifact is written but not kept.** Both workflows upload the diagnostics bundle + only on failure (ADC `e2e.yml:355-359`, Store `:398-404`), and MMCA.Common's `ui-e2e` job sets no + `WEB_VITALS_OUTPUT_DIR` at all and uploads only Playwright traces (`ci.yml:316-322`), so the JSON + lands beside the test binaries and is discarded with the runner. There is no time series: the + sample line in the run log is the only surviving record of a green run. +- **Nothing stops a ceiling being raised to silence a red gate.** As with ADR-060's baseline, the + defaults are a value in source and widening them is a reviewable diff, not a tool-enforced one. + +## Related +[ADR-063](063-accessibility-conformance-gate.md) (the structural sibling: the same package, the same +Playwright suite and the same deploy gate, applied to WCAG 2.1 AA instead of load performance), +[ADR-060](060-performance-regression-gate.md) (the backend half of rubric section 12: a committed +BenchmarkDotNet baseline gating MMCA.Common pull requests, where this gates the two consumer apps' +deploys at the browser), +[ADR-056](056-blazor-render-mode-strategy.md) (the render-mode decision these numbers measure, and +which already cites this suite for the Server-mode-in-CI caveat), +[ADR-015](015-architecture-fitness-functions.md) (the invariant-over-discipline posture applied to +structure, of which this is the client-side runtime-cost instance), +[ADR-041](041-observability-and-telemetry.md) (production telemetry, which observes server-side cost after +deploy where this fails a client-side budget before rollout). diff --git a/docs-src/adr/093-container-image-posture.md b/docs-src/adr/093-container-image-posture.md new file mode 100644 index 0000000..2ed63b4 --- /dev/null +++ b/docs-src/adr/093-container-image-posture.md @@ -0,0 +1,159 @@ +# ADR-093: Container Image Build and Runtime Posture + +## Status +Accepted (2026-08-23) for the three build decisions below. The two runtime postures in "Open +postures" are recorded as **undecided**: they describe what the images do today and the trade-off +each one carries, not a decision to keep doing it. + +## Context +Eleven Dockerfiles produce every deployable container in the two Azure-hosted applications: six in +MMCA.ADC (four services, the Gateway, the Blazor web host) and five in MMCA.Store (three services, +the Gateway, the Blazor web host). They are uniform by copy, not by a shared base file or a template: +each one is a four-stage file with the same shape (`base` on +`mcr.microsoft.com/dotnet/aspnet:10.0`, `build` on `mcr.microsoft.com/dotnet/sdk:10.0`, `publish`, +`final`), and the differences between them are the project path, the `COPY` granularity, and one +publish property (`MMCA.ADC/Source/Services/MMCA.ADC.Conference.Service/Dockerfile:1,6,37,46`). +Nothing in the repositories reconciles them: an edit to one is an edit to one. + +That uniformity encodes three decisions that were made once, defended in Dockerfile comments, and +never written down anywhere a reader of the architecture library would find them. It also leaves two +runtime properties in the state of "what the default gave us", which is worth recording as such so +that a later hardening pass starts from a stated position rather than from a discovery. + +The images are built in CI, not by hand: a fan-out `build-images` matrix job with one leg per image +(`MMCA.ADC/.github/workflows/deploy.yml:795,806-817` for the six ADC legs, +`MMCA.Store/.github/workflows/deploy.yml:792,804-812` for the five Store legs) runs +`docker/build-push-action@v7` over a buildx builder +(`MMCA.ADC/.github/workflows/deploy.yml:834-838`), pushes each image to ACR under both the commit sha +and `latest` (`:844-846`), and caches layers in that same registry with `mode=max` (`:860-861`). +The job runs concurrently with the e2e gate and rolls nothing out; that separation is ADR-080's +subject, not this one's. + +## Decision +**1. The GitHub Packages credential is a BuildKit secret, never an `ARG` or `ENV`.** Both +applications' `nuget.config` source-maps `MMCA.*` to GitHub Packages, so every restore inside an +image needs a token. It arrives as `--secret id=github_token`, mounted into the restore `RUN` +(`.../MMCA.ADC.Conference.Service/Dockerfile:26-28`) and again into the publish `RUN` (`:42-44`), +because publish performs its own restore pass. The value is read out of `/run/secrets/github_token` +into a shell-local `GITHUB_TOKEN` that lives only for that command, which is the variable +`nuget.config` expands. The Dockerfile states the reason in place: a build-arg promoted to `ENV` +lands in image layers, the build cache, and `docker history` (`:8-10`). CI passes it as a +`secrets:` input to the build action, not a `build-args:` input +(`MMCA.ADC/.github/workflows/deploy.yml:852-853`, +`MMCA.Store/.github/workflows/deploy.yml:848-849`), and the workflow repeats the constraint in its +own comments (`MMCA.ADC/.github/workflows/deploy.yml:791-794`). Secret *content* is deliberately not +part of the BuildKit cache key, so rotating the token does not invalidate the restore layer; that is +safe only because the package set is pinned by committed lock files and any +`Directory.Packages.props` change lands in a `COPY` layer that busts the cache anyway (`:847-851`). + +**2. There is deliberately no separate `dotnet build` stage.** The `build` stage restores and stops; +`publish` does its own restore and build. This is a measured decision, dated in the file: on +2026-07-24 (CI run 30115729720) the build stage emitted `bin/Release/net10.0/` while the +ReadyToRun publish emitted `bin/Release/net10.0/linux-x64/`, because the SDK infers a RID for +ReadyToRun. Different paths, so publish never reused build output and every image compiled twice, +about 75 seconds of pure waste per image +(`.../MMCA.ADC.Conference.Service/Dockerfile:30-35`). The Store Dockerfiles carry the same note and +cite the ADC measurement rather than repeating it +(`MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Dockerfile:32-37`). Nothing is lost by dropping the +stage: analyzer gating (`TreatWarningsAsErrors`, `AnalysisMode=All`) runs inside publish. The two +web-host images, which do not use ReadyToRun, drop the stage for the weaker reason that it is one +redundant MSBuild evaluation (`MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/Dockerfile:39-41`, +`MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/Dockerfile:39-41`). + +**3. `PublishReadyToRun=true` on the nine service and gateway images, and only those.** The four ADC +services and the ADC Gateway (`.../MMCA.ADC.Conference.Service/Dockerfile:44`, +`.../MMCA.ADC.Identity.Service/Dockerfile:44`, `.../MMCA.ADC.Engagement.Service/Dockerfile:44`, +`.../MMCA.ADC.Notification.Service/Dockerfile:44`, `MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/Dockerfile:47`) +and the three Store services and the Store Gateway +(`.../MMCA.Store.Catalog.Service/Dockerfile:42`, `.../MMCA.Store.Identity.Service/Dockerfile:42`, +`.../MMCA.Store.Sales.Service/Dockerfile:42`, +`MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Dockerfile:45`) AOT-compile IL at publish time. The two +Blazor web hosts do not (`.../MMCA.ADC.UI.Web/Dockerfile:48`, +`.../MMCA.Store.UI.Web/Dockerfile:47`). The stated purpose is cold start: deploys, restarts and +scale-out replicas skip first-request JIT (`.../MMCA.ADC.Conference.Service/Dockerfile:39-40`), and +the containers those replicas land on are fractional-vCPU Container Apps: 0.25 vCPU / 0.5 GiB for +four of the six ADC apps and 0.5 vCPU / 1 GiB for the other two +(`MMCA.ADC/infra/main.bicep:1074,1273,1399,1542,1688,1794`). On that much CPU, JIT time is not +noise. + +**4. Every image is published with `UseAppHost=false` and started through the shared runtime.** The +`final` stage is the `base` stage plus the publish output, `ENV ASPNETCORE_ENVIRONMENT=Production`, +and `ENTRYPOINT ["dotnet", ".dll"]` +(`.../MMCA.ADC.Conference.Service/Dockerfile:46-50`); the base stage exposes 8080 and 8081, the +REST and h2c gRPC ports of the ADR-012 endpoint profile (`:3-4`). No image installs a package, adds +a shell script, or runs a health-check command of its own: liveness is the Container Apps probe +configured in Bicep. + +## Open postures (undecided) +**The base image is a floating tag, not a digest.** All eleven images start from +`mcr.microsoft.com/dotnet/aspnet:10.0` with no digest pin +(`.../MMCA.ADC.Conference.Service/Dockerfile:1` and the corresponding first line of the other ten), +and build on `mcr.microsoft.com/dotnet/sdk:10.0` the same way (`:6`). The consequence is two-sided +and neither side has been chosen: a rebuild picks up Microsoft's monthly runtime patches with no +action, and a rebuild is also not reproducible, since the same commit built a month apart yields a +different runtime layer. Note the asymmetry with the application layer, which *is* pinned: the +deployment references each image by commit sha, not by `latest` +(`MMCA.ADC/.github/workflows/deploy.yml:845,958-963`). So the code in a revision is exactly +identified and the runtime under it is not. + +**No image drops privileges: all eleven run as root.** No Dockerfile in either repository contains a +`USER` directive, and the `final` stage adds no user (`.../MMCA.ADC.Conference.Service/Dockerfile:46-50`). +The .NET 10 `aspnet` image ships a non-root `app` user, so this is one line per file plus a check +that nothing writes outside the publish directory, not a rearchitecture. It has not been done, and +nothing currently fails because of it: Container Apps runs each container in its own sandbox, so +root inside the container is not root on a shared host. + +Neither posture is currently observed by any gate. The supply-chain job generates its CycloneDX SBOM +from the solution filter (`MMCA.ADC/.github/workflows/deploy.yml:344-353`), so it describes the +NuGet graph, not the image: no image scanner runs in either pipeline, and a base-layer CVE would +therefore not be reported by the checks that gate a deploy. + +## Rationale +- **A secret that is never a layer cannot leak from a layer.** BuildKit secret mounts are the only + mechanism that keeps the credential out of the image, the build cache and `docker history` at the + same time, and the cost is one extra line per `RUN`. The alternative that looks simpler (an `ARG` + plus `ENV`) is the exact anti-pattern the mount exists to replace. +- **The build stage was measured, not assumed.** The decision to drop it rests on an observed + duplicate compile with a named CI run and a number attached, which is also why the comment stayed + in the file: the stage looks like an obvious optimization and would otherwise be re-added by the + next reader. +- **ReadyToRun buys the scarcest resource.** On 0.25 vCPU, first-request JIT is a visible tail on + every deploy and scale-out. Paying it once at publish, in CI, on a runner with more CPU than the + container will ever have, is the trade the deployment topology asks for. +- **The web hosts are excluded for a reason, not by omission.** Their Dockerfiles say so in place: + without ReadyToRun the publish RID path matches the build path, which is what makes the missing + build stage cheap rather than wasteful there. +- **Recording an undecided posture is worth more than implying a decision.** Writing "floating tag, + root user, no image scan" down as open is what lets a later hardening pass argue about the + trade-off instead of rediscovering the state. + +## Trade-offs +- **Eleven copies drift independently.** There is no shared base Dockerfile and no test that + compares them, so a fix applied to one image is applied to one image. The ReadyToRun split (nine + yes, two no) is deliberate, but it means "they are all the same" is already false and a reader + cannot rely on any single file as the canonical one. +- **Full-source copies make the cache coarse.** The service images copy the whole `Source/` tree + before restore (`.../MMCA.ADC.Conference.Service/Dockerfile:23`) because the project-reference + chains through the migrations projects are deep, so any source edit invalidates the restore layer + for those images. The Gateway and web-host images copy individual `.csproj` files first and keep + the finer-grained cache (`MMCA.Store/Source/Hosts/MMCA.Store.Gateway/Dockerfile:21`). +- **ReadyToRun costs build time and image size.** It compiles per RID at publish, which is time + spent on every image build and bytes carried in every layer, in exchange for latency at start. +- **The token is still a token.** Passing it as a secret protects the image, not the feed + relationship: the build still requires a credential with `packages:read` even though the same + packages are published credential-free to nuget.org (ADR-053), because both applications keep + their `MMCA.*` source mapping on GitHub Packages. +- **Root plus a floating base is the compounding one.** Individually each open posture is mild; a + base-layer vulnerability that no scanner reports, in a container whose process runs as root, is + the combination worth closing first. + +## Related +[ADR-038](038-supply-chain-provenance.md) (supply-chain provenance: it gates the **package** graph +with lock files, a vulnerability audit and an SBOM, and stops at the repository boundary, so the +image layers this ADR describes are outside its coverage), +[ADR-080](080-deploy-rollout-revision-rollback.md) (what happens to these images after they are +built: the revision-only rollout and automatic image rollback, whose `build-images` phase is the job +described here), [ADR-064](064-deploy-recency-gates.md) (the proof-of-recency preconditions on a +deploy, none of which observes the image contents, which is the gap the open postures above name), +[ADR-053](053-dual-registry-package-publishing.md) (the dual-registry publishing that makes the credential +in decision 1 a repository-mapping choice rather than a necessity). diff --git a/docs-src/adr/094-client-entity-data-access.md b/docs-src/adr/094-client-entity-data-access.md new file mode 100644 index 0000000..a94dc76 --- /dev/null +++ b/docs-src/adr/094-client-entity-data-access.md @@ -0,0 +1,214 @@ +# ADR-094: Client-Side Entity Data-Access Contract + +## Status +Accepted (2026-08-23). + +## Context +ADR-034 decided the **server** half of entity data access: a generic controller base with a dynamic +query contract, where filters arrive as `filters[Property].operator` / `filters[Property].value` query +pairs bound by `QueryFilterModelBinder` +(`MMCA.Common/Source/Presentation/MMCA.Common.API/ModelBinders/QueryFilterModelBinder.cs:12,20,92`). +It says nothing about who calls that surface or how. + +In practice the calling half is just as decided, and just as load-bearing, but it was never recorded. +Every Blazor and MAUI head in both applications reaches the API through one hand-written class +hierarchy in `MMCA.Common.UI`, and three of its choices are the kind that a new module author copies +without knowing they were choices: the framework ships **hand-written typed bases instead of a +generated client**, the **user-facing retry lives in the client base** rather than in ADR-009's +standard resilience handler, and the `Idempotency-Key` that ADR-017 requires is **minted on the client +and held constant across retry attempts**. ADR-017 specifies only the server filter and says the +client owns the identity of an operation; it never says which client code mints the value or what +keeps it stable. ADR-051 covers only how a bearer token is obtained, stored and refreshed across +render modes, not how an entity request is shaped. + +This record fixes the client-side contract, and (because the same layer owns it) the generic list-page +contract that consumes it. + +## Decision +Client-side entity data access goes through one hand-written base hierarchy in `MMCA.Common.UI`. + +- **One HTTP root: `AuthenticatedServiceBase`** + (`MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/AuthenticatedServiceBase.cs:15`). It owns + the named `"APIClient"` (`:35`), the bearer attachment + (`CreateAuthenticatedClientAsync`, `:59-78`, tolerating the SSR pre-render case where JS interop is + unavailable, `:72-75`), the explicit-token variant used to replay a 401 with a freshly refreshed + token (`CreateClientWithToken`, `:88-95`, the client end of ADR-051), the retry policy, and the + idempotency-key mint. The client itself is registered once, in `AddUIShared` + (`.../MMCA.Common.UI/DependencyInjection.cs:63-82`): base address from `ApiSettings`, `Accept: + application/json`, `AuthDelegatingHandler` plus `CultureDelegatingHandler`, and a transport timeout + pinned to the shared 90-second budget (`:77`, `MMCA.Common/Source/Core/MMCA.Common.Shared/Resilience/HttpResilienceDefaults.cs:19`) + so the BCL's uncoordinated 100-second default cannot cut a call off mid-policy. +- **Typed CRUD is `EntityServiceBase`** + (`.../MMCA.Common.UI/Services/EntityServiceBase.cs:25`), implementing + `IEntityService` (`.../MMCA.Common.UI/Common/Interfaces/IEntityService.cs:12`). + It is a **hand-written typed base over the ADR-034 REST surface, not a generated client**: no Refit, + Kiota or NSwag client generator appears in any of the four repos' `Directory.Packages.props`. +- **The client owns query-string construction for the dynamic query contract.** + `GetPagedAsync` builds `pageNumber`, `pageSize`, `sortColumn`, `sortDirection`, `includeChildren` + (`EntityServiceBase.cs:62-69`) and emits exactly the bracketed filter pairs the server binder parses, + escaping every component (`:71-82`, the pairs at `:77-79`). `GetAllAsync` (`:34-51`), + `GetAllForLookupAsync` (`:92-102`) and `GetByIdAsync` (`:104-120`, with `treatNotFoundAsDefault`) + cover the remaining reads. +- **Retry is owned by the client base, not by a resilience handler.** `RetryPolicy` + (`AuthenticatedServiceBase.cs:26-32`) is a static Polly policy: three retries after the initial + attempt, on `HttpRequestException` or a retryable response, with 2s / 4s / 8s exponential backoff + plus up to 1000 ms of jitter so a fleet of clients does not re-converge on one instant. + `IsRetryableResponse` (`:108-117`) retries 5xx **except** 501 and 505 (permanent verdicts) and adds + 408 and 429 (the server explicitly inviting a later attempt). Every dispatch runs through + `SendRequestAsync` (`EntityServiceBase.cs:183-224`), which passes the caller's `CancellationToken` + into the policy (`:204`) so cancellation aborts the wait between attempts instead of sleeping out + the backoff budget. +- **The `Idempotency-Key` is minted client-side and survives retries.** `NewIdempotencyKey()` returns a + compact GUID (`AuthenticatedServiceBase.cs:51`); the header name is the shared constant + `IdempotencyHeaders.IdempotencyKey` (`MMCA.Common/Source/Core/MMCA.Common.Shared/Http/IdempotencyHeaders.cs:19`). + Only `AddAsync` supplies one (`EntityServiceBase.cs:131-136`): creates are the one CRUD verb that is + not naturally idempotent, so reads, full `PUT` updates and deletes send no key (`:139-163`). The key + is set as a **default header on the single `HttpClient` that serves every attempt** (`:193-200`), + which is what makes the value constant across the retry burst and therefore dedupable by the ADR-017 + filter. Both properties are pinned by tests: the same key on every retry + (`MMCA.Common/Tests/Presentation/MMCA.Common.UI.Tests/Services/EntityServiceBaseIdempotencyRetryTests.cs:91`), + no key on reads, updates or deletes (`:112,123,134`), and the 501-not-retried / 429-retried edges + (`:146,158`). +- **Domain errors are extracted before the generic throw.** `SendRequestAsync` calls + `ServiceExceptionHelper.ThrowIfDomainExceptionAsync` on any non-success response and only then + `EnsureSuccessStatusCode` (`EntityServiceBase.cs:210-213`). The helper + (`.../MMCA.Common.UI/Services/ServiceExceptionHelper.cs:17`) matches the ProblemDetails titles + `Domain Exception`, `Validation Exception` and `Operation failed` (`:49-56`) and rethrows them as + `DomainInvariantViolationException` carrying the server's own message, so a page shows the business + reason rather than "500". A non-JSON body (a bare 401 challenge, an HTML error page) is left to the + caller (`:33-38`). +- **Join entities use `ChildEntityServiceBase`** + (`.../MMCA.Common.UI/Services/ChildEntityServiceBase.cs:17`), the many-to-many sibling: `PostAsync` + (`:24`) and `DeleteByIdAsync` (`:39`, `false` on 404). It shares the bearer helper and the + domain-error extraction but deliberately issues its calls directly, **outside** `RetryPolicy` and + with no idempotency key. +- **Non-CRUD services take the root directly and reuse the same policy.** Services whose endpoints are + not entity CRUD derive from `AuthenticatedServiceBase` itself and call the inherited `RetryPolicy` + by hand, minting a key where the endpoint is `[Idempotent]` (for example + `MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Services/LivePollUIService.cs:96,143` and + `.../SessionQuestionUIService.cs:75`). + +Adoption inventory as of 2026-08-23. **Sixteen production services derive from `EntityServiceBase`**: +nine in ADC Conference (`MMCA.ADC/Source/Modules/Conference/MMCA.ADC.Conference.UI/Services/`: +`ActivityService.cs:11`, `CategoryItemService.cs:11`, `ConferenceCategoryService.cs:11`, +`EventService.cs:14`, `QuestionService.cs:11`, `RoomService.cs:14`, `SessionService.cs:11`, +`SpeakerService.cs:13`, `SponsorService.cs:11`), six in Store (`Catalog.UI/Services/ProductService.cs:15` +and `CategoryService.cs:15`; `Sales.UI/Services/OrderService.cs:14`, `ShoppingCartService.cs:15`, +`InventoryItemService.cs:14`; `Identity.UI/Services/CustomerService.cs:15`), and one inside the +framework itself (`MMCA.Common/Source/Presentation/MMCA.Common.UI/Services/Notifications/PushNotificationService.cs:19`). +**Four derive from `ChildEntityServiceBase`**, all in ADC Conference +(`.../Services/ChildEntityServices.cs:15,31,47,63`). **Sixteen more take `AuthenticatedServiceBase` +directly**: ten in ADC Engagement, four in ADC Conference, ADC Identity's `UserService.cs:15`, and the +framework's `NotificationInboxService.cs:33`. Store has none of that third kind; its one hand-rolled +exception is `CartStateService` +(`MMCA.Store/Source/Modules/Sales/MMCA.Store.Sales.UI/Services/Cart/CartStateService.cs:392`), which +sits outside the hierarchy and re-implements the key mint locally, honoring the same rule (one key per +user action, reused by every attempt, `:83-85`). + +### The list-page contract: `DataGridListPageBase` + +The consumer side of that data-access contract is equally uniform, and is recorded here rather than +separately because the two are used as a pair: a list page inherits the base and hands it a fetch +delegate that is almost always an `EntityServiceBase.GetPagedAsync` call. + +`DataGridListPageBase` +(`MMCA.Common/Source/Presentation/MMCA.Common.UI/Pages/Common/DataGridListPageBase.cs:20`) owns: + +- **Server-side paging through MudDataGrid `ServerData`.** `LoadServerDataAsync` (`:434`) flattens the + grid's filter definitions into the one-filter-per-column dictionary the fetch delegate takes, with + the newest row winning when the user stacks two filters on one column (`ExtractGridFilters`, + `:605-617`), extracts sort from `GridState` (`ExtractSortParameters`, `:619-623`), and converts the + grid's zero-based page to the API's one-based `pageNumber` (`:490`). +- **Cancellation-token management.** Each fetch swaps in a fresh source before tearing down the + previous one, tolerating the `ObjectDisposedException` race a debounced reload after disposal would + otherwise raise (`ResetCancellationTokenAsync`, `:576-598`); during SSR pre-render the token + additionally times out after `PrerenderFetchTimeoutMs` (5000 ms) so a cold backend cannot block the + page load (`:82`, `CreateFetchCts`, `:523-532`). +- **A `LoadFailed` flag that distinguishes error-with-retry from genuinely empty** (`:40`, set at + `:507` and `:565`). A failed fetch renders zero rows, which is visually identical to an empty list + once the error snackbar expires, so pages branch on this flag in `NoRecordsContent` instead of + showing the "no records" state. +- **Viewport-driven mobile card state.** `IsMobile` (`:44`) flips from the browser-viewport observer + (`:263-276`) below the 960 px sidebar-collapse threshold, and the card view has its own paged fetch + path (`MobileItems` / `MobileTotalItems` / `MobileCurrentPage` / `MobilePageSize`, `:47-50`; + `LoadMobileDataAsync`, `:538`). +- **List state persisted and restored three ways.** `ListPageState` + (`.../MMCA.Common.UI/Services/ListPageStateService.cs:9`) carries page, page size, mobile page, sort, + density, filters and scroll position; `ListPageStateService` (`:58`) holds it in memory and mirrors + it to `sessionStorage`, and `ListPageQueryStateService` + (`.../MMCA.Common.UI/Services/ListPageQueryStateService.cs:28`) encodes it into the URL. The URL is + the source of truth on initialization, with the in-memory entry as the fallback and scroll position + read only from it (`DataGridListPageBase.cs:168-205`); writes go to all three (`SaveCurrentState`, + `:625-662`). Deferred writes are dropped once the user has navigated away, because the route is + pinned at initialization rather than read from the live URI at write time (`_ownRoutePath`, `:166`; + `IsOwnRouteCurrent`, `:713-714`). + +**Nineteen types inherit this base**: thirteen in ADC and six in Store, eighteen of them routable list +pages plus ADC's non-routable `AttendeeSearchPanel` +(`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.UI/Pages/CheckIn/AttendeeSearchPanel.razor.cs:16`). +ADR-056 owns the render-mode aspect of the same type (the `PersistentComponentState` pre-render +handoff and the `InteractiveAuto` registration) and carries the same count. + +## Rationale +- **A hand-written typed base beats a generated client here because the surface is already generic.** + ADR-034 collapsed N entity endpoints into one shape, so there is exactly one client shape to write. + A generator would emit N near-identical clients from an OpenAPI document, add a build step and a + regeneration discipline, and still need hand-written policy for retry, idempotency and error + extraction. The typed base gives compile-time DTO safety for the same cost as the generic call. +- **User-facing retry belongs where the user is.** The client base retries a browser-to-gateway call + the user is waiting on, with second-scale backoff a person will tolerate; ADR-009's standard handler + is tuned for server-to-server hops. Keeping them separate lets each move on its own. +- **One key per logical operation is the only version of ADR-017 that works.** The server dedups on the + key; if the client minted a new one per attempt, a retried create would produce a second record, + which is precisely the failure the filter exists to prevent. Setting the header on the client that + serves every attempt makes the invariant structural rather than a rule to remember. +- **Extracting the domain error before `EnsureSuccessStatusCode` is what makes failures speakable.** + The API already returns a specific business reason; without extraction the UI would surface a + generic status-code exception and throw that reason away. +- **The list page is repeated nineteen times, so it is worth a base class.** Paging, cancellation, + filter and sort extraction, viewport switching, error state and state restoration are identical + across every list in both apps; nineteen hand-rolled copies is nineteen chances to get the + cancellation race or the empty-versus-failed distinction wrong. + +## Trade-offs +- **No generated client means drift is caught at runtime, not at build time.** A server-side rename of + a query parameter or a DTO property does not fail the UI build; it fails the call. The mitigation is + that both halves live in one solution and share the DTO types, so the common case (a DTO change) is + a compile error anyway; the exposed case is the query-string vocabulary, which is asserted on each + side separately (`MMCA.Common/Tests/Presentation/MMCA.Common.API.Tests/ModelBinders/QueryFilterModelBinderTests.cs` + for the binder, `EntityServiceBaseTests.cs` for the emitter) rather than end to end. +- **Retry budgets stack across hops, and are deliberately bounded rather than eliminated.** A host that + calls `AddServiceDefaults` applies the standard resilience handler to every factory client through + `ConfigureHttpClientDefaults` (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:48-64`), + including `"APIClient"`. Because the UI base already makes up to four attempts, the shared + per-hop retry count is pinned to **one** + (`HttpResilienceDefaults.cs:21-28`, applied at `Extensions.cs:63`), with the reason stated in both + places: full budgets at every hop turned a backend brownout into an up-to-16x request storm. The + cost is that the effective attempt count for a UI action is a product of two layers and cannot be + read off either one alone. +- **The retry policy is `static` and not configurable.** `RetryPolicy` is a `protected static readonly` + field (`AuthenticatedServiceBase.cs:26`), so its counts and delays are compile-time constants shared + by every service in the process. A per-endpoint or per-environment retry profile would need a change + to the framework, not configuration. +- **Only `AddAsync` gets an idempotency key automatically.** Any non-CRUD write (a hand-written POST on + a service deriving from `AuthenticatedServiceBase`, or code outside the hierarchy such as + `CartStateService`) has to mint and attach the key itself, and nothing fails the build if it does + not. `ChildEntityServiceBase.PostAsync` is one such write and sends no key today. +- **`ChildEntityServiceBase` calls are not retried.** Join add and remove operations get the bearer + token and domain-error extraction but no transient-fault handling, so a blip surfaces to the user + where the same blip on the parent entity would be absorbed. +- **The list-page base is deep.** It coordinates render-mode-aware persistence, three state stores, JS + interop for scroll tracking, and two MudDataGrid v9 parameter-setter workarounds + (`DataGridListPageBase.cs:358-365`, `:393-419`). That depth is the price of nineteen pages behaving + identically, but it makes the base itself the hardest type in the UI package to change safely. + +## Related +[ADR-034](034-generic-entity-query-layer.md) (the server surface this contract calls, and the filter +grammar the client constructs), [ADR-017](017-request-idempotency.md) (the server-side filter whose +client half is specified here: who mints the key and what keeps it constant), +[ADR-051](051-client-auth-token-lifecycle.md) (how the bearer token this base attaches is obtained, +stored and refreshed across render modes), [ADR-009](009-resilience-and-recovery-objectives.md) (the +server-to-server resilience handler whose budget interacts with, but does not replace, the client +retry), [ADR-056](056-blazor-render-mode-strategy.md) (the render-mode aspect of +`DataGridListPageBase`, including the pre-render data handoff and the same nineteen-inheritor +inventory). diff --git a/docs-src/adr/095-soft-delete-unique-indexes.md b/docs-src/adr/095-soft-delete-unique-indexes.md new file mode 100644 index 0000000..49ce64a --- /dev/null +++ b/docs-src/adr/095-soft-delete-unique-indexes.md @@ -0,0 +1,126 @@ +# ADR-095: Uniqueness Under Soft Delete (Filtered Unique Indexes) + +## Status +Accepted (2026-08-23). + +## Context +ADR-005 makes deletion **soft**: an `IAuditableEntity` sets `IsDeleted = true` +(`MMCA.Common/Source/Core/MMCA.Common.Domain/Interfaces/IAuditableEntity.cs:11`) and a named global +query filter hides the row from every query +(`MMCA.Common/Source/Core/MMCA.Common.Infrastructure/Persistence/DbContexts/ApplicationDbContext.cs:336-350`, +the filter name at `:357`). The application therefore behaves as though the row is gone. + +The database does not. A unique index still counts the hidden row, so the deleted record keeps +occupying its unique slot forever: delete a speaker and the email unique index still refuses to +create a new speaker with that email, with an error the user cannot act on because the conflicting +row is invisible to them. That contradiction is stated as the reason for the convention in its own +remarks (`.../Persistence/Conventions/SoftDeleteUniqueIndexConvention.cs:10-21`). + +The obvious fix, a hand-written `HasFilter("[IsDeleted] = 0")` on each index, is the kind of rule +that gets forgotten: it lives in a different file from the `IsDeleted` flag, it is engine-specific +SQL typed as a string literal, and nothing fails when it is missing until a user tries to re-create a +record months later. ADR-005 decides soft-delete versus erasure and never addresses what soft-delete +does to uniqueness; this ADR is that missing half. + +## Decision +Make the filter a **convention**: every unique index on a soft-deletable entity excludes deleted +rows, automatically, in every context of every consumer. + +- **A model-finalizing convention, registered once in the base context.** + `SoftDeleteUniqueIndexConvention` (`.../Conventions/SoftDeleteUniqueIndexConvention.cs:24`) is added + by `ApplicationDbContext.ConfigureConventions` + (`.../DbContexts/ApplicationDbContext.cs:296`, rationale at `:293-295`). Because ADR-006 keeps one + context class per engine over that base, a single registration reaches every module, every database + and every consumer repo. Nothing opts in per entity. +- **Scope: unique, unfiltered, non-owned, soft-deletable.** The convention walks entity types + assignable to `IAuditableEntity` and not owned (`:36-37`, the same predicate the query filter uses + at `ApplicationDbContext.cs:339`), then sets the filter on each index that is unique and declares + none already (`:51-55`). +- **Hand-authored filters win.** An index that already carries a filter is left exactly as written + (`:53`). There is no flag: a configuration that genuinely wants uniqueness across deleted rows too + opts out by declaring its own filter. +- **One predicate builder serves both paths.** `SoftDeleteFilterSql.Build` + (`.../Persistence/SoftDeleteFilterSql.cs:27-38`) is called by the convention (`:47`) and by the + public opt-in `HasSoftDeleteFilter` + (`.../Persistence/Configuration/IndexBuilderExtensions.cs:50-64`), so the automatic and the manual + path cannot disagree about identifier quoting or about which column carries the flag + (`SoftDeleteFilterSql.cs:8-14`). The column name is read from the model, falling back to the + property name (`:32-33`), and the quoting is chosen per engine: brackets for SQL Server, double + quotes otherwise (`:35-37`). +- **SQL Server and SQLite are covered; Cosmos is a no-op.** The convention returns immediately for + `DataSource.CosmosDB` (`:33-34`) and the builder returns `null` for it (`SoftDeleteFilterSql.cs:29-30`), + which the callers read as "leave the index untouched". +- **Non-unique indexes opt in by hand.** `HasSoftDeleteFilter` is the extension point for an index the + convention deliberately skips (`IndexBuilderExtensions.cs:19-30`), and a unique index needing a + second predicate opts in the same way: the two are joined as `{additionalFilter} AND {filter}` + (`:60-63`). The framework's own push-notification dedup index does exactly that + (`.../EntityTypeConfiguration/Notifications/PushNotificationConfiguration.cs:67-69`, reasoned at + `:62-66`), as does Store's SKU index + (`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Infrastructure/Persistence/EntityConfiguration/ProductVariantConfiguration.cs:44-46`). + +The two paths differ in one respect worth knowing: the convention runs at model finalizing, after +module configurations have declared their indexes, while `HasSoftDeleteFilter` reads the column name +at the moment it is called, so a `HasColumnName` on the soft-delete property has to come first +(`IndexBuilderExtensions.cs:31-35`). + +## Rationale +- **The database should agree with what the application shows.** The query filter already says a + soft-deleted row does not exist; a unique index that disagrees is the one place the illusion leaks, + and it leaks as an unexplainable error rather than as a visible row. +- **A convention beats per-configuration discipline.** Every unique index on a soft-deletable entity + wants this predicate, so making it the default is strictly better than asking each configuration + author to remember engine-specific filter SQL. +- **A shared builder is what makes the opt-in path trustworthy.** Because the manual call routes + through the same `Build`, a hand-filtered index gets the same column name and the same quoting the + convention would have produced, instead of a SQL-Server-shaped literal that silently breaks on + SQLite. +- **Respecting a hand-authored filter is not politeness, it is correctness.** Overwriting one would + have dropped the `[DedupKey] IS NOT NULL` clause the push-notification index depends on, which is + precisely why that configuration opts in explicitly. +- **The behavior is pinned by tests, in both directions.** + `MMCA.Common/Tests/Core/MMCA.Common.Infrastructure.Tests/Persistence/Conventions/SoftDeleteUniqueIndexConventionTests.cs` + asserts the filter is applied (`:30-37`), that a soft-deleted row no longer blocks re-inserting the + same value (`:40-61`), that a **live** duplicate is still rejected (`:64-74`), and that a + hand-authored filter survives (`:77-84`). The opt-in path is pinned for both engines and for Cosmos + in `.../Persistence/Configuration/IndexBuilderExtensionsTests.cs:23-60`, and the combined predicate + in `.../Persistence/Configuration/PushNotificationConfigurationTests.cs:27-30`. + +## Trade-offs +- **It moves schema in consumers, invisibly from the entity configuration.** Adopting the convention + is a database-contract change: nothing in an entity configuration changed, but the next scaffolded + migration drops and recreates unique indexes. The v1.120.0 adoption did exactly that in ADC, for + `IX_User_Email` + (`MMCA.ADC/Source/Hosting/MMCA.ADC.Migrations.SqlServer.Identity/Migrations/20260720031638_CommonV1120OutboxLeaseAndSoftDeleteIndexFilters.cs:14-43`) + and for `IX_CategoryItem_CategoryId_Name` + (`.../MMCA.ADC.Migrations.SqlServer.Conference/Migrations/20260720031645_CommonV1120OutboxLeaseAndSoftDeleteIndexFilters.cs:14-43`), + each needing an `EXPAND-CONTRACT-OVERRIDE` marker to pass the ADR-057 gate. The churn is uneven: + Store's and Helpdesk's migrations of the same sweep carry only the outbox columns + (`MMCA.Store/Source/Hosting/MMCA.Store.Migrations.SqlServer.Catalog/Migrations/20260720031626_CommonV1120OutboxLeaseAndSoftDeleteIndexFilters.cs:12-27`, + `MMCA.Helpdesk/Source/Hosting/MMCA.Helpdesk.Migrations.SqlServer.Tickets/Migrations/20260720031655_CommonV1120OutboxLeaseAndSoftDeleteIndexFilters.cs:12-27`), + because Store's configurations already opted in by hand and Helpdesk's single module declares no + unique index on a soft-deletable entity. So "the framework changed your schema" is true for some + consumers and not others, and only the generated migration says which. +- **Duplicates among deleted rows become legal, permanently.** Any number of soft-deleted rows may + now share the same "unique" value. Two consequences follow: a report that reads with + `ignoreQueryFilters: true` can see duplicates that the model's index name promises are impossible, + and any restore path a consumer writes (flipping `IsDeleted` back to `false`) has to handle a + collision with the live row that took the slot, because the database will reject it at that moment + rather than at delete time. +- **Engine coverage is partial by construction.** The guarantee exists only where the provider + supports a filtered or partial index. On Cosmos both paths return without touching the index + (`SoftDeleteUniqueIndexConvention.cs:33-34`, `SoftDeleteFilterSql.cs:29-30`), so a Cosmos-backed + source (ADR-018) does not get this behavior at all, and a model shared across engines gets a + different uniqueness contract per engine. +- **The filter is not visible where the index is declared.** Reading + `builder.HasIndex(x => x.Email).IsUnique()` does not reveal that the shipped index is filtered; the + predicate first appears in a generated migration or a model snapshot. That is the cost of moving + the rule out of the configuration files, and the reason the tests above assert on + `index.GetFilter()` rather than on behavior alone. + +## Related +ADR-005 (decides soft-delete over erasure and owns the query filter that hides the row, but says +nothing about uniqueness: this ADR closes that gap), ADR-057 (the expand/contract CI gate, which +classifies the drop-and-recreate this convention produces as a legitimate override and cites the ADC +Identity migration as its live marker at `057-expand-contract-schema-evolution-gate.md:70-77`), +ADR-006 (one context class per engine over the shared `ApplicationDbContext`, which is why a single +convention registration reaches every module and every database). diff --git a/docs-src/adr/096-best-effort-side-effects.md b/docs-src/adr/096-best-effort-side-effects.md new file mode 100644 index 0000000..85e59a3 --- /dev/null +++ b/docs-src/adr/096-best-effort-side-effects.md @@ -0,0 +1,152 @@ +# ADR-096: Best-Effort Side-Effect Contract + +## Status +Accepted (2026-08-23). + +## Context +A command that has already committed often has follow-up work attached to it: evict the output-cache +entries the write invalidated, broadcast the new state to a live channel, send the notification the +user is waiting for. That work can fail on its own, and when it does the question is not whether to +retry but what the failure is allowed to do to the caller. Turning it into an exception would roll +back, retry or 500 an operation whose real work already succeeded. + +Five records each answer that question locally, for their own feature, and each answer is right: +ADR-024 makes a push delivery failure non-fatal and records `MarkAsFailed` instead +(`024-push-notifications.md:55`), ADR-026 makes cross-service cache eviction best-effort so a broken +eviction store cannot dead-letter a coherence hint, ADR-076 degrades a data-subject export per section +rather than failing the package (`076-data-subject-export.md:74`), ADR-091 composes the reset email in +the handler and delivers it best-effort, "awaited and its failure caught, logged and swallowed" +(`091-cache-backed-password-reset.md:71-76`), and ADR-054 makes compensation best-effort per order +line (`054-saga-compensation-and-reconciliation.md:158`). What none of them decides is the **policy**: +which failures may be swallowed at all, at what severity, whether cancellation counts as one of them, +and how a swallow is made visible to somebody who is not reading the log. Answered per call site, that +produces a repo full of hand-rolled `catch (Exception)` blocks, each choosing its own severity, its +own treatment of cancellation and its own decision to count nothing. ADR-041 records the counter this +record's helper emits and notes that it is wired to no alert (`041-observability-and-telemetry.md:175-180`, +`:197-200`), but it records the instrument, not the contract behind it. + +## Decision +One framework helper defines the contract, and a swallow that does not go through it is a deliberate, +documented exception. + +- **`BestEffort.ExecuteAsync(operation, logger, action, cancellationToken = default)`**, a static + helper in the Application layer + (`MMCA.Common/Source/Core/MMCA.Common.Application/Services/BestEffort.cs:45-49`), runs the side + effect and absorbs its failure. +- **The action is awaited, not fire-and-forget.** The helper awaits `action(cancellationToken)` + (`:57`), so the side effect completes (or fails) before the caller continues; nothing is left as an + orphan task racing the response. +- **A failure produces exactly one Warning.** `BestEffortLog.DispatchFailed` is a source-generated + `[LoggerMessage]` at `Warning` (`:81-84`) reading "Best-effort operation '{Operation}' failed and + was swallowed; the caller's outcome is unaffected" (`:83`), with the exception attached. The catch + is deliberately broad, with the reason written into the code: the whole contract is that nothing the + side effect throws reaches the caller (`:65-71`). +- **A failure produces exactly one metric increment.** `besteffort.dispatch.failed` is a `Counter` + on its own meter, `MMCA.Common.BestEffort` (`:102`, instrument at `:107-115`), incremented with an + `operation` tag (`:115`). It is a meter of its own rather than a counter folded into + `MMCA.Common.Cqrs`, because best-effort dispatch is not part of the CQRS pipeline and an operator can + drop or keep it independently of the RED metrics (`:93-97`). The Aspire service defaults subscribe it + (`MMCA.Common/Source/Hosting/MMCA.Common.Aspire/Extensions.cs:170`). +- **The operation name is a low-cardinality constant.** It becomes a metric tag (`:22`), so call sites + pass a `const` or a fixed prefix plus a value from a small fixed set. A blank name throws + `ArgumentException` and a null logger or action throws `ArgumentNullException` (`:51-53`): the helper + swallows the side effect's failures, never its own caller's bugs. +- **Cancellation is not swallowed.** An `OperationCanceledException` raised while the caller's own + token is cancelled is rethrown (`:59-63`), so a host shutdown or an abandoned request unwinds + promptly instead of being recorded as a spurious side-effect failure. A cancellation that is not the + caller's (an inner timeout) is a genuine failure of the side effect and is swallowed like any other. +- **Post-commit work passes `CancellationToken.None` on purpose.** The write has committed, so its + follow-up must outlive a caller that has already walked away + (`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.API/OutputCacheEvictionExtensions.cs:34-40`; + the ADC broadcasts take the parameter's default for the same reason, + `.../SubmitQuestionHandler.cs:122-124`). +- **The contract is pinned by tests.** `BestEffortTests` covers the transparent success path, token + passthrough, one-Warning-per-failure, the `operation`-tagged increment observed through a + `MeterListener`, the rethrow of the caller's cancellation, the swallow of a non-caller cancellation, + and argument validation + (`MMCA.Common/Tests/Core/MMCA.Common.Application.Tests/Services/BestEffortTests.cs:15-141`). + +Adoption today is **seven production call sites across two apps**. Six are in ADC Engagement: the +live-channel drain worker, whose operation name is the prefix `live-channel-publish:` plus the work +item's event name and whose own catch turns the rethrown cancellation into a quiet stop +(`MMCA.ADC/Source/Modules/Engagement/MMCA.ADC.Engagement.Infrastructure/Live/LiveChannelPublishProcessor.cs:36`, +`:45-58`, `:60-65`); three session-question broadcasts, `session-question-submit-broadcast` +(`.../SessionQuestions/UseCases/Submit/SubmitQuestionHandler.cs:34`, call at `:131`), +`session-question-moderation-broadcast` (`.../UseCases/Moderate/ModerateQuestionHandler.cs:30`, call at +`:136`) and `session-question-upvote-broadcast` +(`.../DomainEventHandlers/SessionQuestionUpvoteChangedHandler.cs:45`, call at `:52`); the poll-results +broadcast `livepoll-results-broadcast` +(`.../LivePolls/DomainEventHandlers/LivePollVoteChangedHandler.cs:44`, call at `:51`); and the +cross-host cache eviction `bookmark-cache-evict-broadcast` +(`.../UserSessionBookmarks/DomainEventHandlers/UserSessionBookmarkCacheEvictionHandler.cs:56`, call at +`:68-80`). The seventh is Store Catalog's `TryEvictByTagAsync` extension, whose operation name is the +prefix `catalog.output-cache-evict:` plus the tag +(`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.API/OutputCacheEvictionExtensions.cs:18`, +`:30-41`); four controllers evict through it (`Controllers/CategoriesController.cs:157`, +`ProductsController.cs:222`, `ProductVariantsController.cs:129`, `ProductImagesController.cs:281`). + +Two swallows deliberately stay hand-rolled, and both say so in code. Store's `AddVariantHandler` +publishes `ProductVariantChanged` after the commit and catches around it, logging at **Error** with +the `ProductId` and `ProductVariantId`: the event is lost, Sales does not auto-create the zero-stock +inventory record, and an operator needs those ids to create it by hand. Routing it through the helper +would both downgrade an unrecoverable loss to Warning and drop the ids, which is why that one site +stays as it is while every other swallow in the repo uses the helper +(`MMCA.Store/Source/Modules/Catalog/MMCA.Store.Catalog.Application/Products/UseCases/AddVariant/AddVariantHandler.cs:81-95`, +catch at `:97-108`). The framework's own `OutputCacheEvictionHandler` hand-rolls the same +swallow-log-count shape against `cache.eviction.failed` on the `MMCA.Common.OutputCache` meter +(`MMCA.Common/Source/Presentation/MMCA.Common.API/Caching/OutputCacheEvictionHandler.cs:51-62`), +because `BestEffort` lives in Application and the API package cannot reach it without a layer-crossing +reference or a duplicated meter name (recorded in ADR-026, `026-caching-strategy.md:412-417`). + +## Rationale +- **One policy beats five local leniencies.** Each feature record is still right about its own + degradation; what they could not each decide is the shape of the swallow. A single helper fixes + severity, cardinality and the cancellation rule once, so a new post-commit side effect inherits them + instead of re-litigating them. +- **A swallowed failure has to be countable.** A Warning in a log nobody reads is how a side effect + quietly stops working for weeks. The counter turns "the broadcast has been failing since Tuesday" + into a question a dashboard can answer, which is the only thing that makes swallowing defensible. +- **Cancellation is not a failure.** Swallowing it would turn an orderly shutdown into a burst of + spurious warnings and a metric spike, and would let a stopping host keep doing work it was told to + stop. Rethrowing keeps shutdown a shutdown. +- **Awaiting keeps the failure attributable.** A detached task would still fail, just later, off the + request's context and without the logger scope that names what it was doing. +- **Fixing the severity at Warning is a filter, not a limitation.** A swallow that genuinely deserves + Error, with ids an operator must act on, is evidence the work is not best-effort. `AddVariantHandler` + is exactly that case, and it stays outside the helper. + +## Trade-offs +- **Nothing gates use of the helper.** There is no fitness rule, analyzer or architecture test that + fails a build for a hand-rolled `catch (Exception)` that should have been a `BestEffort` call; the + helper is a convention backed by review. The only inventory is a search, which is how the seven call + sites and two deviations above were enumerated. +- **The Warning carries the operation name and the exception, nothing else.** No entity id, no + correlation payload beyond the ambient scope. `SubmitQuestionHandler` records that cost explicitly: + the question id is one log line earlier, not in the best-effort warning + (`.../Submit/SubmitQuestionHandler.cs:125-127`). +- **The counter is failure-only and alerts on nothing.** A healthy system emits zero, and zero is + indistinguishable from a host that never wired the meter. ADR-041 puts it in exactly that gap + (`041-observability-and-telemetry.md:197-200`). +- **The meter name is a duplicated literal.** `MMCA.Common.Aspire` subscribes it by string because that + package has no reference to Application (`BestEffort.cs:89-92`, `Extensions.cs:170`), so a rename has + to move in two places or the metric silently stops being exported. +- **A swallow is still a loss.** The helper decides that the caller does not see the failure; it does + not make the side effect happen. A cache entry heals on its own TTL, but a lost broadcast never + replays, and callers whose loss is unrecoverable have to say so themselves. +- **Two "best effort" counters exist.** The layer constraint on the API package means + `cache.eviction.failed` and `besteffort.dispatch.failed` count the same shape of event on different + meters, so an operator asking "what is silently failing" has two places to look. + +## Related +[ADR-024](024-push-notifications.md) (push delivery failure is non-fatal and recorded rather than +raised, one of the local leniencies this policy generalizes), +[ADR-026](026-caching-strategy.md) (eviction is best-effort, and its `OutputCacheEvictionHandler` is +the framework's one documented non-reuse of this helper), +[ADR-041](041-observability-and-telemetry.md) (records `besteffort.dispatch.failed`, its meter, and +that it is wired to no alert), +[ADR-054](054-saga-compensation-and-reconciliation.md) (compensation is best-effort per line, and its +one hand-rolled swallow is the same question answered locally), +[ADR-076](076-data-subject-export.md) (per-section degradation makes an incomplete package the +contract rather than a failure), +[ADR-091](091-cache-backed-password-reset.md) (the reset email is awaited, caught, logged and +swallowed, the shape this helper standardizes). diff --git a/docs-src/adr/README.md b/docs-src/adr/README.md index 4d07055..698ec93 100644 --- a/docs-src/adr/README.md +++ b/docs-src/adr/README.md @@ -19,7 +19,7 @@ pattern they describe: they capture context and trade-offs that aren't obvious f | [012](012-grpc-host-transport.md) | gRPC-host transport convention | Two coherent profiles; the Kestrel choice forces the gateway-forward mode + JWKS routing. **Both consumers default to Profile A** (`Http2`-only h2c) after Store converged on 2026-06-22; Profile B (`Http1AndHttp2` + ALPN) is retained only for the SignalR/WebSocket hosts (ADC's Notification service and Store's Sales service). Since 2026-07-09 ADC's Notification runs a **mixed-endpoint profile**: `Http1AndHttp2` default endpoint plus a dedicated `Http2`-only named `grpc` endpoint for the ADR-039 live-channel push ingress. Corrected 2026-07-25: gateway-routed JWKS is the local Aspire wiring only, both consumers inject the direct in-cluster Identity authority in production; and probe posture splits three ways (ADC hosts get a dedicated Http1-only probe listener, Store's `Http2`-only hosts use TCP probes). Updated 2026-08-07: the per-service Kestrel wiring is now one shared framework method, `ConfigureEndpointsWithHealthProbe` in `MMCA.Common.Aspire` (`redeclareCleartextEndpoint: false` expresses the mixed profile), and ADC's Notification maps a second authorized gRPC service (user-notification export) on the same `grpc` endpoint. Updated 2026-08-14: Store's Sales also runs the mixed-endpoint profile through the same shared helper (plus an authorized user-sales-export gRPC service), so no deployed host is pure Profile B any longer. | | [013](013-result-pattern.md) | Result pattern over exceptions | Expected failures are `Result`/`Result` values with a transport-agnostic `ErrorType`; only the edge maps to HTTP/gRPC. Exceptions stay for the genuinely exceptional, and the exceptional path has its own edge contract: an ordered `IExceptionHandler` chain (OperationCanceled, Domain, DbUpdate, Validation, Global; registration order load-bearing) emitting RFC 9457 ProblemDetails (revised 2026-07-21). | | [014](014-cqrs-decorator-pipeline.md) | CQRS decorator pipeline | Thin `ICommandHandler`/`IQueryHandler` use cases behind a Scrutor decorator chain; the order is load-bearing. Revised 2026-07-19: business failures (`Result.Failure`) now roll the transaction back like exceptions, and in-process event dispatch is deferred until after commit. Revised 2026-08-18: **the order changed**, to FeatureGate -> Authorization -> Logging -> Caching -> Validating -> Timeout -> Transactional -> Handler (queries: FeatureGate -> Authorization -> Logging -> Caching -> Timeout -> Handler). Authorization is keyed on `IRequiresPermission` and resolves through `IPermissionRegistry` + `ICurrentUserService.Roles`, returning a Forbidden error and counting `cqrs.authorization.denied.count`; it sits outside caching deliberately, so a denied request neither reads nor populates the cache. Timeout is keyed on `IHasTimeout`, links a budget token to the caller's, fails with code `Request.TimedOut` and counts `cqrs.timeout.count`, while caller cancellation still propagates through an exception filter. The order is now pinned by the shipped `DecoratorPipelineOrderTestsBase` rather than by comments alone. | -| [015](015-architecture-fitness-functions.md) | Architecture fitness functions | Invariants gate the build twice: a compile-time layer guard (MSBuild) + a shared NetArchTest rule library parameterized by `IArchitectureMap`, run identically across all four repos (Common / Store / ADC / Helpdesk). Revised 2026-08-18: two new rule families plus a **third enforcement layer**. `NamespacesHaveNoDependencyCycles` finds strongly connected components in each layer assembly's namespace graph from the type signature surface, with the `AllowedCycleNamespaces` hook checked against the **whole component** so an allowance cannot hide a new cycle (Common exempts one Infrastructure tangle, `root -> Settings -> Persistence -> root`, justified per edge). `AsyncMethodsDeclareTrailingCancellationToken` requires public `Task`/`ValueTask` methods in Application and Infrastructure to end in `CancellationToken cancellationToken`, with automatic exemptions for `Dispose`/`DisposeAsync`, compiler-generated and special-name members, and overrides or implementations of externally-declared signatures; the two real findings (`NotificationHub.JoinChannelAsync`/`LeaveChannelAsync`) are exempted as SignalR wire contracts but were made cancellable via `Context.ConnectionAborted`. The third layer is a compile-time public API surface gate (`Microsoft.CodeAnalysis.PublicApiAnalyzers` 5.6.0 wired once in `Directory.Build.props` for 14 of the 15 `Source` projects, UI.Maui documented as excluded on build-topology grounds): RS0016/RS0017 over `PublicAPI.Shipped`/`Unshipped` baselines holding 5,150 declarations, errors by inheritance from the repo's global analyzer-error default rather than by their own entry, so widening or breaking a package surface becomes a reviewable text diff; RS0026/RS0027/RS0041 off with recorded reasons. The baseline shipped in v1.153.0, so the gate is live for consumers from that release on. Formalizes what the `consumer-source-build` canary only sampled. All three layers run in the required `build-and-test` gate. Revised again 2026-08-18 (Section B): two further consumer-facing rule families. `ArchitectureRules.Protos` gives the gRPC wire contract the freeze integration events already had, pinning the package, every rpc with both streaming flags, every field with its number and every enum value, while deliberately leaving `syntax`, `import` and every `option` unpinned (the line is "would a deployed peer notice", so a `csharp_namespace` change passes the gate even though it breaks generated client code); MMCA.Common ships no `.proto` and exercises the rule through a matched clean/drifted fixture pair instead. The idempotency-intent gate (see [ADR-017](017-request-idempotency.md)) joins beside it. Counts now **104 methods across 36 bases, 99 executed by Common itself** (`FACTS.md:44,47`), superseding the 102/34/87 figures above. | +| [015](015-architecture-fitness-functions.md) | Architecture fitness functions | Invariants gate the build twice: a compile-time layer guard (MSBuild) + a shared NetArchTest rule library parameterized by `IArchitectureMap`, run identically across all four repos (Common / Store / ADC / Helpdesk). Revised 2026-08-18: two new rule families plus a **third enforcement layer**. `NamespacesHaveNoDependencyCycles` finds strongly connected components in each layer assembly's namespace graph from the type signature surface, with the `AllowedCycleNamespaces` hook checked against the **whole component** so an allowance cannot hide a new cycle (Common exempts one Infrastructure tangle, `root -> Settings -> Persistence -> root`, justified per edge). `AsyncMethodsDeclareTrailingCancellationToken` requires public `Task`/`ValueTask` methods in Application and Infrastructure to end in `CancellationToken cancellationToken`, with automatic exemptions for `Dispose`/`DisposeAsync`, compiler-generated and special-name members, and overrides or implementations of externally-declared signatures; the two real findings (`NotificationHub.JoinChannelAsync`/`LeaveChannelAsync`) are exempted as SignalR wire contracts but were made cancellable via `Context.ConnectionAborted`. The third layer is a compile-time public API surface gate (`Microsoft.CodeAnalysis.PublicApiAnalyzers` 5.6.0 wired once in `Directory.Build.props` for 14 of the 15 `Source` projects, UI.Maui documented as excluded on build-topology grounds): RS0016/RS0017 over `PublicAPI.Shipped`/`Unshipped` baselines holding 5,150 declarations, errors by inheritance from the repo's global analyzer-error default rather than by their own entry, so widening or breaking a package surface becomes a reviewable text diff; RS0026/RS0027/RS0041 off with recorded reasons. The baseline shipped in v1.153.0, so the gate is live for consumers from that release on. Formalizes what the `consumer-source-build` canary only sampled. All three layers run in the required `build-and-test` gate. Revised again 2026-08-18 (Section B): two further consumer-facing rule families. `ArchitectureRules.Protos` gives the gRPC wire contract the freeze integration events already had, pinning the package, every rpc with both streaming flags, every field with its number and every enum value, while deliberately leaving `syntax`, `import` and every `option` unpinned (the line is "would a deployed peer notice", so a `csharp_namespace` change passes the gate even though it breaks generated client code); MMCA.Common ships no `.proto` and exercises the rule through a matched clean/drifted fixture pair instead. The idempotency-intent gate (see [ADR-017](017-request-idempotency.md)) joins beside it. Counts are owned by `FACTS.md` and this record stops restating them: as of framework v1.160.0 it reads **110 test methods across 38 abstract `*TestsBase` classes** (`FACTS.md:44`), of which MMCA.Common's own build executes **129** (`FACTS.md:47`), superseding both the 102/34/87 and the 104/36/99 figures above. The executed figure now exceeds the shipped method count because it also counts Common's own direct tests, so the two are no longer a subset and its container. | | [016](016-lockstep-versioning-masstransit-pin.md) | Lockstep versioning + MassTransit-v8 pin | All packages release at one version (count owned by FACTS.md); consumers swept in one pass (no phased rollout). MassTransit is pinned to v8 (v9 needs a license) and the pin is a fitness-function build gate. | | [017](017-request-idempotency.md) | HTTP request idempotency | `[Idempotent]` action filter dedups client retries via an `Idempotency-Key` header + cached replay (24h, `X-Idempotent-Replay`); distinct from ADR-003's handler idempotency. Revised 2026-08-18: the opt-in is no longer silent. `[NonIdempotent(justification)]` joins `[Idempotent]` and a fitness gate (`IdempotencyConventionTestsBase`, inherit-aware, concrete classes only) fails any `[HttpPost]` declaring neither, converting the audit-the-inventory trade-off into a declared decision. `AuthControllerBase` marks register `[Idempotent]` and login/refresh/revoke plus the OAuth exchange `[NonIdempotent]`, because ADR-050 rotates the refresh token on every use, so a replayed token response would hand back a pair already revoked. The no-op-without-a-key contract is now pinned by tests, which is what makes annotating an existing endpoint invisible to existing clients. Scope limits recorded: POST only (PUT/PATCH are outside the gate), and the justification string is required to exist, not to be a reason. | | [018](018-polyglot-persistence.md) | Polyglot persistence (per-engine sources) | Three storage engines (SQL Server / Cosmos / SQLite) behind one model; engine is a `[UseDataSource]` attribute on the entity config (the orthogonal `Engine` axis to ADR-006's `Name` axis). Plumbing shipped + tested; first non-SQL entity not yet in production. | @@ -52,14 +52,14 @@ pattern they describe: they capture context and trade-offs that aren't obvious f | [045](045-managed-file-storage-and-avatars.md) | Managed file storage + user avatars (BR-116a) | `IFileStorageService` (Null default; Azure Blob impl via `AddAzureBlobFileStorage` when the `FileStorage` section is complete - `ServiceUri` = DefaultAzureCredential, `ConnectionString` = Azurite) + `IImageProcessor`/`ImageSharpImageProcessor` (decode, auto-orient, exact-square crop, strip ALL metadata, re-encode JPEG - only pixels survive untrusted uploads) + `IMediaPickerService` UI capability (native pick/capture; web heads render InputFile instead). Avatar contract: 2 MB in, 256x256 JPEG out, `avatars/{userId}-{random8}.jpg` in a public-read container, `[Pii]` URL nulled + blob deleted on anonymize. | | [046](046-http-api-versioning.md) | HTTP API versioning strategy | One `AddCommonApiVersioning` call wires header-based versioning (`api-version` reader, default 1.0 assumed when unspecified, supported/deprecated versions reported on every response); `ServiceInfoControllerBase` ships a live v1.0-deprecated + v2.0 exemplar and the shared `ServiceInfoVersioningContractTestsBase` fitness contract asserts the headers per repo. Adopted by every extracted ADC/Store service host plus Helpdesk. The HTTP-contract axis, distinct from ADR-010's integration-event schema versioning. Amended 2026-08-12 (v1.146.0): the same call registers `ApiParameterDescriptorBackfillProvider`, so an unbound route token (`{version}`, `{tenant}`) can no longer 500 the OpenAPI document. | | [047](047-soft-deleted-user-session-revocation.md) | Soft-deleted-user session revocation | `SoftDeletedUserMiddleware` (BR-133, after authentication, before authorization) returns 401 for authenticated callers whose `User.IsDeleted`, via `ISoftDeletedUserValidator` behind a 30-second cached check (`SoftDeletedUserCache.MarkerDuration`); the validator is lazily resolved so hosts that do not register one (non-Identity services, Helpdesk) no-op, and cache or validator failure deliberately fails open. Since 2026-08-07 the record reflects the shared generic `SoftDeletedUserValidator` in MMCA.Common (no per-app query classes) and the adoption asymmetry: ADC writes an immediate deletion marker on delete, Store relies on the passive 30-second TTL. Bounds the stateless-JWT revocation window (ADR-004) to roughly the cache duration instead of the token lifetime. | -| [048](048-primitive-identifier-type-aliases.md) | Primitive identifier type aliases | Entity IDs are primitives behind per-module `global using {Entity}IdentifierType = ...` aliases, linked solution-wide via `Directory.Build.props`, chosen over DDD strongly-typed ID structs: readable signatures with zero EF/serializer/OpenAPI friction, at the cost of no compile-time protection against swapping two same-typed IDs. Wrapper structs were considered and deferred: no wrapper-struct identifier type exists in any repo. **Revisited by [ADR-085](085-identifier-type-aliases-revisited.md)** (2026-08-18): the deferral is re-evaluated, priced (43 aliases, 42 of them `int`; 3,641 occurrences across 1,016 files to migrate) and upheld, now with three named revisit triggers instead of an open-ended "not now". | +| [048](048-primitive-identifier-type-aliases.md) | Primitive identifier type aliases | Entity IDs are primitives behind per-module `global using {Entity}IdentifierType = ...` aliases, linked solution-wide via `Directory.Build.props`, chosen over DDD strongly-typed ID structs: readable signatures with zero EF/serializer/OpenAPI friction, at the cost of no compile-time protection against swapping two same-typed IDs. Wrapper structs were considered and deferred: no wrapper-struct identifier type exists in any repo. **Revisited by [ADR-085](085-identifier-type-aliases-revisited.md)** (2026-08-18): the deferral is re-evaluated, priced (44 aliases, 43 of them `int`; 3,192 occurrences across 1,001 files to migrate) and upheld, now with three named revisit triggers instead of an open-ended "not now". | | [049](049-library-configureawait-policy.md) | Library-scoped ConfigureAwait(false) policy | Packaged non-UI framework code awaits with `ConfigureAwait(false)`, enforced as a build gate (CA2007 = warning for `Source/**` in MMCA.Common's .editorconfig repo-delta, UI component packages excluded); the application repos keep ConfigureAwait analyzers off. Protects the MAUI head (ADR-042) and any non-ASP.NET consumer from library context-capture deadlocks. | | [050](050-jwt-refresh-token-rotation.md) | JWT + single rotating refresh token | One issuance workflow in `AuthenticationServiceBase`: a short-lived stateless JWT access token (default 15 min) plus one server-stored, opaque refresh token per user that rotates on every use; a presented token that mismatches the stored one (or is expired) triggers `RevokeRefreshToken` and 401. Each rotation re-stamps the expiry (`Jwt:RefreshTokenExpirationDays`, default 7 days, honored since 2026-07-21), so the lifetime is a sliding inactivity window, not an absolute session cap; refresh is bound to the same principal via `GetPrincipalFromExpiredToken`. Single-token-per-user means a new login signs out other devices' refresh chains. | | [051](051-client-auth-token-lifecycle.md) | Client-side auth token lifecycle across render modes | One `ITokenRefresher` abstraction with two head-specific strategies: browser heads (Server/WASM) refresh through the same-origin proxy (`SameOriginProxyTokenRefresher`, HttpOnly cookie via `/auth/session/token`, ADR-022) while MAUI refreshes directly against the API (`DirectApiTokenRefresher`) persisting the rotated pair in OS SecureStorage; `AuthDelegatingHandler` stamps the bearer on the APIClient pipeline, render-mode-aware `ITokenStorageService` implementations (WASM in-memory / Server SSR-vs-interactive, single-flight with 30s skew) hold the tokens, and `JwtAuthenticationStateProvider` drives Blazor auth state. Client half of ADR-022/ADR-050. Revised 2026-08-07: the MAUI SecureStorage implementation is the shared `MauiTokenStorageService` in `MMCA.Common.UI.Maui` (`AddCommonMauiTokenStorage()`), consumed by both apps instead of per-app copies. | -| [052](052-background-job-execution.md) | Background job execution (bounded queue + hosted drain) | In-process work that outlives a request runs as a bounded `Channel` singleton plus a `SingleReader` `BackgroundService` drain, never an untracked `Task` started from a controller: the host can then cancel and await it on shutdown instead of a deploy killing it mid-run. Full mode encodes what the work is worth (`DropOldest` for ephemeral broadcasts, whose drops are only visible through the `itemDropped` callback since `TryWrite` always succeeds; `Wait` plus non-blocking `TryWrite` for expensive runs, which refuse rather than discard). Expensive work dedups by natural key across queue AND execution, refusing duplicates with 409. Post-commit work attaches to a domain event so ADR-003 deferral supplies the commit boundary. Instances: `LiveChannelPublishQueue` (ADR-039), `SessionScoringQueue`. | +| [052](052-background-job-execution.md) | Background job execution (bounded queue + hosted drain) | In-process work that outlives a request runs as a bounded `Channel` singleton plus a `SingleReader` `BackgroundService` drain, never an untracked `Task` started from a controller: the host can then cancel and await it on shutdown instead of a deploy killing it mid-run. Full mode encodes what the work is worth (`DropOldest` for ephemeral broadcasts, whose drops are only visible through the `itemDropped` callback since `TryWrite` always succeeds; `Wait` plus non-blocking `TryWrite` for expensive runs, which refuse rather than discard). Expensive work dedups by natural key across queue AND execution, refusing duplicates with 409. Post-commit enqueue takes two shapes, not one: two call sites attach to a domain event so ADR-003 deferral supplies the commit boundary, and four enqueue from the command handler on the line after the save, which is genuinely post-commit only because no Engagement command implements `ITransactional` (so there the ordering is a rule a future edit must keep rather than one the pipeline keeps for it). Instances: `LiveChannelPublishQueue` (ADR-039), `SessionScoringQueue`. | | [053](053-dual-registry-package-publishing.md) | Dual-registry package publishing (trusted publishing) | Every release pushes the same nupkgs to nuget.org **and** GitHub Packages from one tag, because the GitHub Packages NuGet registry requires a `read:packages` PAT even for public packages, so the documented `dotnet add package MMCA.Common.API` failed for everyone outside the account. nuget.org becomes the documented install path and the only public download signal; GitHub Packages is retained as a mirror. Auth is **keyless**: each publishing job exchanges its GitHub OIDC token via `NuGet/login@v1` for a one-hour key, authorized by a nuget.org policy pinned to the permanent GitHub ids of owner, repository, and workflow file (nuget.org now marks API keys "Not recommended"). No stored secret, therefore no rotation and no expiry-failure mode; the workflow file name becomes load-bearing in exchange. A `github.repository_owner` guard keeps forks releasing to GitHub Packages. Listing metadata (`PackageProjectUrl`, `PackageIcon`, `PackageTags`, per-package `Description`, packed README) is part of the deliverable. No backfill: nuget.org starts at the first release after this decision. | | [054](054-saga-compensation-and-reconciliation.md) | Saga compensation + reconciliation backstop | Cross-boundary consistency without two-phase commit, the question ADR-003 / ADR-006 / ADR-021 each leave open. Each workflow step raises a domain event and the compensating action lives in its own handler (`OrderCancelledSagaHandler`, `OrderPaymentFailedSagaHandler`) running in its own DI scope, so it commits after (not inside) the originating transaction. Idempotency is a persisted aggregate marker (`Order.InventoryRestored`) committed by the SAME `SaveChanges` as the compensating writes, which is stronger than ADR-021's record-after-success inbox; concurrent redeliveries are serialized by the ADR-035 `RowVersion` token. A periodic `PaymentReconciliationService` sweep is the saga-timeout backstop, asking Stripe for authoritative session status and driving the same guarded transitions a lost webhook would have, losing races to the webhook by design. Adopted in MMCA.Store's Sales module only. See [ADR-086](086-process-manager-deferred.md) (2026-08-18) for the orchestrated alternative: deferred with a recorded shape and trigger, and with this sweep remaining underneath any future coordinator. | -| [055](055-repository-and-specification-contract.md) | Repository + Specification data-access contract | The read contract is ISP-split into `IEntityReader` (id lookups) and `IEntityQuerier` (collections, projections, counts), composed by `IReadRepository`, which alone exposes the raw `IQueryable` surfaces. `ApplicationLayer_DoesNotUseRawQueryableSurfaces` fails the build on `.Table` / `.TableNoTracking*` in Application code (opt-in per repo: Common, ADC and Store today, with a documented `AllowedFiles` ratchet; Store adopted 2026-07-28 with an empty allowlist), because a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary later. Predicates are composable `Specification` expression trees (And / Or / Not / Inline) fed into the same query pipeline. The split is shipped but not yet consumed: `IUnitOfWork` still hands out only the composites. Referenced but never decided by ADR-018 / ADR-033 / ADR-035 / ADR-048. Revised 2026-08-18 (**substantive, five changes**): `QuerySpecification` gives a specification ordering, include paths, paging, tracking and a scoped soft-delete-filter escape, superseding the predicate-only trade-off; composition drops `Expression.Invoke` for a parameter-rebinding `ExpressionVisitor` (composed once per instance) and gains fluent `And`/`Or`/`Not` extension members, retiring the provider-bet trade-off; `IEntityQuerier` gains specification-first `ListAsync` (plus a projecting overload), `CountAsync` and `AnyAsync`, and `IEntityQueryService` widens from the abstract `Specification` to `ISpecification`; an optional `IEntityDTOProjector` pushes DTO projection into SQL via `ExecuteProjectedAsync` when a projector is registered, the read is untracked and no include crosses a data source, falling back silently otherwise; and keyset pagination arrives as `GetPageByCursorAsync` (`KeysetPageRequest`/`KeysetCollectionResult`, a versioned base64url cursor, `Result`-based validation failures) alongside a correctness fix that makes **paginated** reads deterministic (Id-ordered by default, Id tie-break appended to a caller sort; unpaginated reads stay deliberately unordered). | +| [055](055-repository-and-specification-contract.md) | Repository + Specification data-access contract | The read contract is ISP-split into `IEntityReader` (id lookups) and `IEntityQuerier` (collections, projections, counts), composed by `IReadRepository`, which alone exposes the raw `IQueryable` surfaces. `ApplicationLayer_DoesNotUseRawQueryableSurfaces` fails the build on `.Table` / `.TableNoTracking*` in Application code (opt-in per repo: Common, ADC and Store today, with a documented `AllowedFiles` ratchet; Store adopted 2026-07-28 with an empty allowlist), because a raw-queryable handler is EF-coupled and cannot move behind a gRPC boundary later. Predicates are composable `Specification` expression trees (And / Or / Not / Inline) fed into the same query pipeline. The split is shipped and now consumed in both applications (twelve read-only declarations narrowed across eight files in ADC, three in Store, with a holder that genuinely needs both halves deliberately keeping the composite), acquired by implicit conversion from `GetReadRepository<,>()` rather than by DI: `IUnitOfWork` still hands out only the composites, and no registration was added for the narrow interfaces because a container-resolved one would bypass the unit of work's data-source resolution and per-scope cache. Referenced but never decided by ADR-018 / ADR-033 / ADR-035 / ADR-048. Revised 2026-08-18 (**substantive, five changes**): `QuerySpecification` gives a specification ordering, include paths, paging, tracking and a scoped soft-delete-filter escape, superseding the predicate-only trade-off; composition drops `Expression.Invoke` for a parameter-rebinding `ExpressionVisitor` (composed once per instance) and gains fluent `And`/`Or`/`Not` extension members, retiring the provider-bet trade-off; `IEntityQuerier` gains specification-first `ListAsync` (plus a projecting overload), `CountAsync` and `AnyAsync`, and `IEntityQueryService` widens from the abstract `Specification` to `ISpecification`; an optional `IEntityDTOProjector` pushes DTO projection into SQL via `ExecuteProjectedAsync` when a projector is registered, the read is untracked and no include crosses a data source, falling back silently otherwise; and keyset pagination arrives as `GetPageByCursorAsync` (`KeysetPageRequest`/`KeysetCollectionResult`, a versioned base64url cursor, `Result`-based validation failures) alongside a correctness fix that makes **paginated** reads deterministic (Id-ordered by default, Id tie-break appended to a caller sort; unpaginated reads stay deliberately unordered). | | [056](056-blazor-render-mode-strategy.md) | Blazor render-mode strategy | `InteractiveAuto` is declared once on the root router of each web head (no per-page `@rendermode` anywhere) and prerendering stays on, so an interactive page renders SSR, then Server, then WASM. The resulting double fetch is removed once, in the shared `DataGridListPageBase`, by persisting the prerendered payload via `PersistentComponentState` (the persist callback declares `InteractiveAuto` explicitly, and the prerender fetch is time-bounded at 5000 ms); detail pages skip the prerender fetch instead, and one page family hand-rolls its own copy. Both runtimes register the same service set, which the WASM-compatible layer rule in `MMCA.Common.LayerEnforcement.targets` is what makes possible. `InteractiveServer` is pinned only under E2E config flags. Not uniform: MMCA.Helpdesk and the Common UI gallery are Server-only with no `.Client` project, and nothing enforces the root mode. Taken as given context by ADR-022 / ADR-027 / ADR-028 / ADR-051, none of which decided it. | | [057](057-expand-contract-schema-evolution-gate.md) | Expand/contract schema evolution as a CI gate | A migration added by a PR may not call `DropColumn` / `DropTable` / `DropIndex` inside `Up()` without an `EXPAND-CONTRACT-OVERRIDE` marker, because deploy rollback is revision-only and never reverts schema: the previous release has to keep running against the new one. "Added by this PR" is `git diff --diff-filter=A` against `origin/...HEAD`, path-scoped, `Designer` files skipped and the pre-split frozen archives out of scope; only the `Up()` range is scanned. Documented as `// EXPAND-CONTRACT-OVERRIDE: ` but enforced as a bare substring match, so one occurrence exempts every destructive operation in that migration. A PR-only merge gate, not a deploy gate: MMCA.ADC since 2026-07-19, ported to MMCA.Store 2026-07-25. MMCA.Helpdesk has neither the gate nor a deploy workflow and carries an unmarked `DropIndex`; MMCA.Common has no migrations at all. ADR-030 decides who applies migrations, never what shape one may take. | | [058](058-runtime-conformance-suites-as-a-package.md) | Runtime conformance suites shipped as a package | `MMCA.Common.Testing` exports six abstract contract bases (ProblemDetails, OpenAPI, service-info versioning, security headers, graceful shutdown, decorator order) that a consuming host subclasses to prove it wired the framework's runtime contracts correctly. They run against a really booted host (`WebApplicationFactory` plus a GUID-named throwaway SQL database, Respawn between tests), not by reflection over registrations, which is the boundary against ADR-015: that record scopes itself to structure and registration and says so. Subclasses stay thin (usually two probe requests or a resource list). Adoption is partial and named as such: OpenAPI on every service host, ProblemDetails on all seven REST hosts (ADC Notification closed the gap 2026-08-13), versioning on one host per repo, and security headers plus graceful shutdown on the Gateways only, so no service host asserts either. MMCA.Helpdesk adopts exactly one, the decorator suite (2026-08-18); the other five HTTP-facing bases have no Helpdesk subclass. | @@ -74,7 +74,7 @@ pattern they describe: they capture context and trade-offs that aren't obvious f | [067](067-ui-module-shell-composition.md) | Shared Blazor shell + `IUIModule` composition | The framework package ships the router, layout, nav menu and routable shell pages (login, register, home, not-found, forbidden, notification inbox); each module plugs in by implementing `IUIModule` (`NavItems`, `Assembly` for `AdditionalAssemblies` route discovery, app-bar and layout component extension points), and `Routes.razor` enumerates the registered modules at runtime. The UI-layer counterpart of ADR-059's `IModule`; adopted by every ADC/Store module, Common's Notification module and the UI gallery. MMCA.Helpdesk keeps its own shell and does not adopt it. | | [068](068-value-objects-as-validated-primitives.md) | Value objects as validated domain primitives | Seven sealed `record` value objects over an abstract `ValueObject` base (Email, Money, Currency, PhoneNumber, Address, DateRange, DateTimeRange), each a private constructor plus a `Result`-returning `Create` factory (fitness-enforced); shared constraints live in `*Invariants` classes where reused. EF mapping is `OwnsMoney` (owned type) or value converters (no schema change); `Currency.None` is the null-object sentinel on materialization; JSON/XML round-trip via `[JsonConstructor]`, converters and DataContract, gRPC via a hand-mapped `MoneyV1`. The deliberate opposite of ADR-048's primitive identifier aliases: identifiers cross boundaries, domain values carry invariants. | | [069](069-shared-data-protection-key-ring.md) | Shared DataProtection key ring for scaled-out hosts | `AddCommonDataProtection` persists the DataProtection key ring to one Azure blob under `DefaultAzureCredential` so cookies and antiforgery tokens minted by one replica decrypt on another; key-ring encryption at rest (Key Vault) is a deliberately independent second gate, so a lagging role assignment cannot take authentication down. Absent config is a full no-op (dev/test/Helpdesk take no Azure dependency). Adopted by ADC (Identity service + UI.Web, container on the existing avatar storage account) and, since 2026-08-13, by Store's UI host, which provisions its own dedicated private storage account gated behind a `dataProtectionStorageReady` parameter (flipped true in production). | -| [070](070-fail-fast-configuration-contract.md) | Fail-fast configuration contract | Every settings section binds through `AddOptions().Bind().ValidateDataAnnotations().ValidateOnStart()` so a misconfigured host refuses to boot instead of failing at first use: 11 framework sections plus two per service host across all four repos. Settings consumed above Infrastructure go through read-only singleton facades (`IApplicationSettings`, `ISmtpSettings`, `IJwtSettings`) instead of `IOptions`; direct `IOptions` injection stays legitimate inside Infrastructure and UI hosts. Nothing gates the chain today (recorded trade-off). | +| [070](070-fail-fast-configuration-contract.md) | Fail-fast configuration contract | Every settings section binds through `AddOptions().Bind().ValidateDataAnnotations().ValidateOnStart()` so a misconfigured host refuses to boot instead of failing at first use: 16 framework registrations (twelve of them in the Infrastructure package alone) plus exactly two per service host, identically across all eight hosts in the three application repos, with six framework bindings deliberately off the chain because an absent section there is a working default rather than a misconfiguration. Five settings types are additionally exposed as read-only singleton facades (`IApplicationSettings`, `IJwtSettings`, `IConnectionStringSettings`, `ISmtpSettings`, `IPushNotificationSettings`), but the facade is the required form only where a settings type is read above Infrastructure, and the "Application layer names no `IOptions`" invariant no longer holds: the framework's own `ForgotPasswordHandlerBase` takes `IOptions`, and no consumer repo's production code uses a facade at all. Nothing gates the chain today (recorded trade-off). | | [071](071-barcode-scanning-and-qr-display.md) | Barcode scanning + QR display capability | Two halves of one feature split by what each actually depends on. Rendering a QR needs no device, so `QrCodeImage` is a plain shared component (QRCoder's managed PNG path as a base64 data URI, `[EditorRequired]` `AltText`, a framework-owned `QrErrorCorrectionLevel` so consumers are not pinned to the generator). Reading one needs a camera, so `IBarcodeScannerService` (`IsSupported` + `ScanAsync`, never throws, `null` covers cancelled/denied/unsupported) is an ADR-042 capability with a TryAdd null fallback and a MAUI implementation over ZXing.Net.MAUI (MIT, modal scan page, 2D formats, Android/iOS). The native half is opt-in per head (`UseCommonBarcodeScanner(cancelText, cameraDescription)`) and deliberately NOT folded into `UseMauiDeviceCapabilities`, so a head that never scans ships neither the handler nor a camera permission declaration; the trade-off is that both strings resolve at builder time, so a runtime culture switch does not re-localize the scan page. | | [072](072-qr-badge-check-in-and-points.md) | QR badge check-in + points gamification (ADC) | The badge QR carries an **opaque** server-verified `Guid` (`mmca-adc:badge:{credential}`), not a JWT or HMAC: the scanning device is online anyway, while a signed token leaks its claims to a screenshot and cannot be revoked without a second store, where a GUID is revoked by one `Regenerate()`. Organizers scan attendees as the primary path, with two recorded self-service exceptions since 2026-08-14 (attendee-scanned sponsor and room QRs, each behind its own feature flag, low-value and once-per-subject), and TicketLeap keeps owning arrival check-in, so ADC's scope is session-first: one `CheckIn` aggregate with an `Event`/`Session`/`Sponsor` scope, guarded by three filtered unique indexes. Points are an append-only ledger whose unique `(UserId, ActivityType, SubjectKey)` index is simultaneously the redelivery-idempotency guard and the anti-farming rule (one award per subject, so five questions in a session earn once). Awards ride events that already exist (check-in and feedback via the outbox, question-asked via an in-module domain event, at-most-once and accepted as such); rule values live in config and a 0 disables a rule. The ledger stays ADC-local with the extraction path pre-paid (`IPointsAwarder` names no conference concept), and the leaderboard is opt-in with a display-name snapshot. | | [073](073-multi-tenancy-model.md) | Multi-tenancy (shared-schema + DB-per-tenant) | A second **named** EF query filter, `"Tenant"`, composes by AND with the existing `"SoftDelete"` filter and embeds the executing context as a constant, so one cached model per source serves every tenant with the tenant id as a SQL parameter (null tenant = system context sees all). `ITenantContext` resolves claim-then-header behind `TenantResolutionMiddleware` (wired after `UseAuthentication`, `RequireTenant` fail-closed), a dedicated `TenantSaveChangesInterceptor` stamps writes and refuses cross-tenant ones, and DB-per-tenant is a per-tenant `PhysicalDataSource` connection-string override under the same `DataSourceKey`. The outbox is drained per `(source, tenant)` pair with no `OutboxMessage` schema change, and the caching decorators prefix keys with the tenant. Helpdesk is the reference adopter; ADC and Store stay single-tenant. | @@ -87,15 +87,20 @@ pattern they describe: they capture context and trade-offs that aren't obvious f | [080](080-deploy-rollout-revision-rollback.md) | Rollout + automatic revision rollback | Both consumer deploys are single-revision rollouts whose last step is a post-deploy smoke gate asserting expected status codes (health, JWKS, a public read, exactly 401 on protected routes); on failure it walks every container app back to its previous revision (`az containerapp revision copy`) and escalates loudly when a rollback itself fails, since that splits the fleet across revisions. Rollback is revision-only by construction: schema is never reverted (no down-migrations, ADR-030), which is the premise ADR-057's expand/contract gate is built on. Best-effort loop, hand-maintained app list, and a previous-revision selector that can overshoot by one release are recorded trade-offs. | | [081](081-cost-baseline-deploy-gate.md) | Cost baseline as a deploy gate | A read-only `cost-guard` workflow (cron + dispatch + `workflow_call`) asserts the production footprint still matches its cost baseline: every Container App at `maxReplicas` 2 or below and every SQL database on the accepted tier (ADC Basic only; Store Basic or Standard). It sits in `deploy.needs`, so an un-reverted manual surge blocks the next production deploy; it never mutates anything. Sibling of ADR-060/062/064 (ADR-064 only enumerates it in passing). Recorded defects: the printed remediation is circular now that the gate blocks the deploy it recommends re-running, Store's tier check passes any S-series scale-up, and an unreadable replica count scores `ok` (fails open). | | [082](082-two-tier-cors-posture.md) | Two-tier cross-origin posture | Service hosts get named allow-listed CORS policies from one `AddCommonCors` call (origins from `Cors:AllowedOrigins`, five explicit methods, four headers, credentials), selected per environment inside the shared pipeline between routing and authentication; the gateways get a DEFAULT policy that restricts only origins and passes any header/method, because a reverse proxy must forward arbitrary client headers. Both tiers carry a Development allow-any-origin branch under an S5122 suppression. Origins are config, empty by default, injected at deploy time (gateways only in production). No test asserts an emitted `Access-Control-*` header, and `Cors:AllowedOrigins` is deliberately outside the ADR-070 fail-fast chain. | -| [083](083-crud-lifecycle-event-taxonomy.md) | CRUD lifecycle event taxonomy | One `EntityChangedEvent` base (a `DomainEntityState` discriminator plus the entity id) replaces per-entity Created/Updated/Deleted triples: the factory raises `Added`, mutators raise `Updated`, `Delete()` raises `Deleted`, and handlers filter on `State`. Business state-machine transitions (`OrderPaid`, `ShoppingCartCheckedOut`) deliberately keep their own event types off this base. The discriminator rides integration events as a frozen wire field (ADC's points handlers branch on it). 13 derivations across the four repos (plus 2 in the ECommerce sample); 32 event types follow the shape overall, so the base is the convenience and the shape is the convention, and nothing enforces the taxonomy structurally (recorded trade-off). ADR-003 decides dispatch and ADR-010 versioning; neither decided the taxonomy. | +| [083](083-crud-lifecycle-event-taxonomy.md) | CRUD lifecycle event taxonomy | One `EntityChangedEvent` base (a `DomainEntityState` discriminator plus the entity id) replaces per-entity Created/Updated/Deleted triples: the factory raises `Added`, mutators raise `Updated`, `Delete()` raises `Deleted`, and handlers filter on `State`. Business state-machine transitions (`OrderPaid`, `ShoppingCartCheckedOut`) deliberately keep their own event types off this base. The discriminator rides integration events as a frozen wire field (ADC's points handlers branch on it). 14 derivations across the four repos (plus 2 in the ECommerce sample, 16 in all); 33 event types follow the shape overall (23 in ADC, 9 in Store, 1 in Helpdesk), so the base is the convenience and the shape is the convention, and nothing enforces the taxonomy structurally (recorded trade-off). ADR-003 decides dispatch and ADR-010 versioning; neither decided the taxonomy. | | [084](084-stripe-webhook-ingress.md) | Stripe webhook ingress contract | The inbound-from-a-third-party leg of the delivery family (ADR-003 out, ADR-021 broker in, ADR-054 the backstop): an anonymous raw-body POST verified by `Stripe-Signature`, whose status code encodes ACCEPTED-vs-PROCESSED rather than success/failure. 400 goes back only when the event cannot be accepted at all (exactly three rejection codes), because rejections make Stripe retry and eventually disable the endpoint, silently stopping every payment update (the incident that motivated the record); post-acceptance processing failures log and return 200, with ADR-054's reconciliation sweep as the backstop. A startup `BackgroundService` self-registers the endpoint with Stripe, deletes only its own `Auto-registered by MMCA` stale endpoints, and holds the freshly minted signing secret in a volatile singleton while logging it at Critical for an operator to persist. Store Sales only (the ADR-072 single-module precedent). | -| [085](085-identifier-type-aliases-revisited.md) | Identifier type aliases revisited (revisits 048) | The wrapper-struct alternative ADR-048 deferred is re-evaluated, priced and **deferred again**, now against named triggers instead of open-endedly. The exposure is measured rather than asserted: 43 aliases across 10 files in the four repos, 42 of them resolving to `int`, so the compiler sees one identifier type with 42 synonyms and the bite lands on cross-module scalar references (ADC's `CheckIn` constructor takes two different `UserIdentifierType` arguments that transpose silently). The migration is priced at 3,641 occurrences across 1,016 files in the four `Source` trees, tests excluded, on a lockstep-released package family, which is what keeps the aliases. Three triggers re-open it: a production defect traced to a transposition, a greenfield fifth consumer, or a materially growing cross-module reference graph. Buys no safety: it converts a blind spot into a priced deferral. | +| [085](085-identifier-type-aliases-revisited.md) | Identifier type aliases revisited (revisits 048) | The wrapper-struct alternative ADR-048 deferred is re-evaluated, priced and **deferred again**, now against named triggers instead of open-endedly. The exposure is measured rather than asserted: 44 aliases across 10 files in the four repos, 43 of them resolving to `int`, so the compiler sees one identifier type with 43 synonyms and the bite lands on cross-module scalar references (ADC's `CheckIn` constructor takes two different `UserIdentifierType` arguments that transpose silently). The migration is priced at 3,192 occurrences across 1,001 files in the four `Source` trees, tests excluded, on a lockstep-released package family, which is what keeps the aliases. Three triggers re-open it: a production defect traced to a transposition, a greenfield fifth consumer, or a materially growing cross-module reference graph. Buys no safety: it converts a blind spot into a priced deferral. | | [086](086-process-manager-deferred.md) | Process manager deferred (relates to 054) | A documented deferral, shipping no code: the shape a durable multi-step workflow coordinator would take (a MassTransit v8 saga state machine, durable per-instance correlation state in the owning service's own database, per-instance deadlines instead of ADR-054's fixed-interval sweep, compensating transitions calling the same guarded domain methods), the licensing pin that fixes the technology choice (MassTransit held at 8.5.10 because v9 needs a commercial license), and the trigger: build it when the first workflow appears with three or more steps across two or more services, state that does not fit one aggregate, and at least one per-instance deadline. Verified absent today (no `MassTransitStateMachine` / `ISaga` in any repo). ADR-054's compensation plus outbox retries suffice until then, and its sweep stays underneath a coordinator rather than being replaced by one. | | [087](087-broker-poison-message-handling.md) | Broker poison-message handling (amends 009) | Three scoped fixes for the two broker failures retry cannot answer. **Second-level redelivery** is transport-aware and the asymmetry is the decision: `MessageBusSettings.EnableDelayedRedelivery` defaults to **false** because RabbitMQ needs the `rabbitmq_delayed_message_exchange` plugin the Aspire dev container lacks (a default-on flag would break every developer's first `F5` at bus start), while Azure Service Bus applies `UseDelayedRedelivery` without consulting the flag because it schedules natively; intervals default to `[60, 600, 3600]`. **`FaultIntegrationEventConsumer`**, auto-registered by `RegisterIntegrationEventConsumer` (opt out with `registerFaultConsumer: false`), makes an exhausted message visible with one Error log and a counter, and deliberately never replays it. A new meter `MMCA.Common.Broker` carries `broker.fault.count` and `broker.circuit.open.count`. A **Polly circuit breaker wraps only the outbox broker publish** (0.5 failure ratio, 10 minimum throughput, 30s sampling, 15s break, no retry paired since the outbox is the retry); `BrokenCircuitException` takes the normal re-lease path and differs only in observability. A per-query **database breaker is recorded as rejected**: it does not compose with EF's `EnableRetryOnFailure` execution strategy, so the EF retry posture stands. | | [088](088-gateway-edge-responsibilities.md) | Gateway edge responsibilities (extends 019) | The three cross-cutting behaviors the edge owns, and the three it declines. ADR-079 scoped the gateways out of the shared service pipeline, correctly, and that left correlation, rate limiting and readiness unowned at the one process every request passes through. The edge kit ships in `MMCA.Common.Aspire` (a YARP host references nothing else): `GatewayCorrelationMiddleware` **ensures** `X-Correlation-ID` and writes it onto the forwarded request so the downstream adopts it instead of minting a second id, and is context-free by construction (its only dependency is the `RequestDelegate`); `AddGatewayRateLimiting` runs a per-client-IP fixed window (120/60s) that **includes anonymous callers**, chained with a global concurrency cap (200), deliberately inverting ADR-019's anonymous exemption because the output cache that justified it lives behind the proxy; `AddGatewayDownstreamHealthChecks` probes each downstream's `/alive` on the **`Ready` tag only**, so a downstream outage stops traffic without restarting a healthy gateway. Bypasses are two-tier: `/health`, `/alive` and `/.well-known` unconditionally, plus configurable prefixes for the Stripe webhook (a 429 makes Stripe disable the endpoint) and SignalR hubs. Unknown IP fails open. Declined and recorded with a trigger: **edge JWT pre-validation** (ADR-004 keeps sole validation authority; two validators are two truths across a key rotation). Settings honor ADR-070 on **both** construction paths: the configuration overload binds through `ValidateDataAnnotations().ValidateOnStart()`, and because the limiter closes over an eagerly-bound copy (so a caller can bypass the options pipeline entirely), the shared overload also runs `Validator.ValidateObject` at registration, making the `[Range]` bounds load-bearing rather than decorative. Trade-offs recorded: the closed-over copy means no `IOptionsMonitor` reload (a limit change is a restart); the limiter is per replica; nothing gates adoption, though both consumer gateways wire all three registrations today. | | [089](089-gateway-topology-owned-by-configuration.md) | Gateway topology owned by configuration (amends 008) | The route table is data. Both gateways previously built it by hand in code (26 `MapForwarder` calls in ADC, 10 in Store), and the argument for moving was that **one table was described three times in one repository and the three already disagreed**: ADC registered 15 conference forwarders, its comment said 16, and its `RouteMapTests` pinned 23 of 26, leaving `/Sponsors`, `/CheckIns` and `/Points` ungated; Store had no route test at all. What is genuinely elsewhere is **not** a route table and must not become one: the AppHost and bicep hold service address books (name to URL, no path prefixes). Decision: `ReverseProxy` configuration is the single route source, the AppHost keeps dev orchestration and bicep keeps deployment topology, `RouteMapTests` becomes a drift gate in both repos, the per-destination HTTP version policy (ADR-012 profiles) moves into cluster config, and Store's missing total activity timeout is fixed in the same move. **Shipped 2026-08-18 in both consumers**, with two recorded residuals: Store's suite is one-way (an added route that nobody pins still passes) and `ForwardHttp2` is an ADC configuration key while Store reads the code default. Trade-offs: the compiler stops helping (a bad cluster name becomes a runtime 502, which is why the test is part of the decision), catch-all matching semantics shift, and a JSON table reviews less well than a code diff. | | [090](090-event-upcaster-registration.md) | Event upcaster registration extension point (completes 010) | The consumer-side half of ADR-010 becomes a mechanism: `IEventUpcaster` (a pure payload mapping from a retired contract to its successor) registered per module via `AddEventUpcaster<...>()` (TryAddEnumerable idiom), assembled into a chaining `EventUpcasterRegistry` that walks V1 -> V2 -> V3 by declared target type, preserves `MessageId`/`DateOccurred` across every hop (inbox dedup keys survive by construction), and fails startup, naming the offenders, on duplicate sources, self-maps or cycles. Both delivery paths consult it: `DomainEventDispatcher` upcasts before selecting integration handlers (covers monolith mode and pre-upgrade outbox rows), and broker hosts drain a retired type via the dedicated `RegisterUpcastedIntegrationEventConsumer()`, so handlers are written once against the newest contract. Two new fitness functions on `EventConventionTestsBase` (unique source types; target `SchemaVersion` strictly higher) are inherited by every consumer tree with no edit. Deferred and recorded: outbox type-name aliasing for prematurely deleted types, and producer-side rewriting of old rows. | | [091](091-cache-backed-password-reset.md) | Cache-backed password reset (extends 029/032) | The reset credential is a cache record, not three columns on the user row and not a self-contained signed payload: single use is unimplementable without a store anyway, and TTL plus attempt cap are native to the cache, so the feature reached two production apps with **no migration**. `IPasswordResetTokenService` mirrors `LoginProtectionService` down to the address normalization (the load-bearing part: raw-input keys give `User@x.com` and `user@x.com` independent tokens for one account), and adds 256-bit tokens, SHA-256 at rest, `FixedTimeEquals`, one live token per address, and a wrong guess that rewrites the record with its **remaining** lifetime so guessing cannot extend the window. The anti-enumeration contract lives in `ForgotPasswordHandlerBase`, whose every path returns success (unknown address, throttle, failed send), with `ResetPasswordHandlerBase` collapsing every rejection into one `Auth.InvalidResetToken` 401; a subclass cannot leak the distinction by accident. `PasswordResetAuthControllerBase` is an additive **sibling** of `AuthControllerBase` (both apps' `AuthController` already occupies that inheritance chain) routed to the same `Auth` prefix, so no gateway route changes; both actions are `AllowAnonymous` + `[Idempotent]` + `auth-ip`. Email is composed in the handler behind overridable hooks (link **and** raw token, because the MAUI head cannot deep link), English only, fire-and-log with no outbox. Residuals recorded: cache eviction kills live links, and the duration of a registered-address request still correlates with existence, so the timing oracle the always-202 contract does not close is named rather than implied. | +| [092](092-web-vitals-budget-gate.md) | Core Web Vitals as a shipped test contract and deploy gate | The client-side complement of ADR-060: a benchmark measures allocations in one process and k6 measures what the server returned, and neither reaches the browser. `WebVitalsCollector` in `MMCA.Common.Testing.E2E` installs `PerformanceObserver` hooks as a Playwright init script so they exist before first paint (no third-party JS, no network egress, a missing entry type leaves that metric at 0 rather than throwing), and `WebVitalsBudget` defaults to the Core Web Vitals good band: LCP 2500, FCP 1800, TTFB 800 ms, CLS 0.1 and a single-interaction INP-sample ceiling of 500. An absolute client-side ceiling is defensible where ADR-060 refused an absolute nanosecond count, because the good band is externally defined and the measured values sit 4x to 30x below it; a breach throws naming the metric, the measured value and the page, and a zero INP is skipped rather than read as a pass. Both deployed apps assert the shipped defaults over four surfaces each, calibrated against recorded maxima with the run id beside the ceiling, and because `e2e.yml` runs the whole project with no filter the budget rides the existing chromium-only `e2e-gate` in `deploy.needs` at zero marginal CI cost, the lever ADR-063 used for accessibility. The framework gallery runs the same collector under three looser local constants as a catastrophic-regression backstop. Recorded costs: the gate is ui-scoped and may legitimately skip, it never runs on a pull request, CI pins the UI to `InteractiveServer` so the numbers are not production's `InteractiveAuto`, a marginal breach can be retried away, LCP and CLS are Chromium-only (two of MMCA.Common's three engines assert them against nothing), coverage is a hand-picked page list, and the green-run artifact is discarded with the runner. MMCA.Helpdesk adopts none: it pins the package and has no E2E project, the same gap ADR-063 records. | +| [093](093-container-image-posture.md) | Container image build and runtime posture | Eleven Dockerfiles (six ADC, five Store) uniform by copy rather than by a shared base, encoding three decisions never written down and two properties left at "what the default gave us". Decided: the GitHub Packages credential is a BuildKit `--secret` mount read into a shell-local variable, never an `ARG` promoted to `ENV` that would land in the layers, the build cache and `docker history`; there is deliberately **no** separate `dotnet build` stage, because the SDK infers a RID for ReadyToRun so publish never reused build output and every image compiled twice (measured at about 75 seconds per image, dated in the file with its CI run id), and nothing is lost since analyzer gating runs inside publish; and `PublishReadyToRun=true` on the nine service and gateway images and only those, because their Container Apps run on 0.25 to 0.5 vCPU where first-request JIT is a visible tail on every deploy and scale-out. Every image publishes `UseAppHost=false`, starts through `ENTRYPOINT ["dotnet", ".dll"]`, and exposes the ADR-012 REST and h2c ports, with liveness left to the Container Apps probe. Recorded as **undecided** rather than as decisions: the base image is a floating `10.0` tag while the application layer is pinned by commit sha (so the code in a revision is exactly identified and the runtime under it is not), and all eleven containers run as root with no `USER` directive. No image scanner runs in either pipeline, so a base-layer CVE is reported by nothing that gates a deploy, and that combination is named as the one worth closing first. | +| [094](094-client-entity-data-access.md) | Client-side entity data-access contract | The calling half of ADR-034, decided long ago and recorded only now, because a new module author copies its three choices without knowing they were choices. One hand-written base hierarchy in `MMCA.Common.UI`, **not** a generated client (no Refit, Kiota or NSwag in any of the four repos): `AuthenticatedServiceBase` owns the named `APIClient`, the bearer attachment, the 90-second transport timeout that keeps the BCL's uncoordinated 100-second default from cutting a call off mid-policy, and the retry policy; `EntityServiceBase` emits exactly the bracketed `filters[Property].operator` pairs the server binder parses. User-facing retry lives in the client base rather than in ADR-009's server-to-server handler: three retries at 2s/4s/8s plus jitter, 5xx except the permanent 501 and 505, plus 408 and 429. The `Idempotency-Key` is minted client-side and set as a default header on the single `HttpClient` that serves every attempt, which makes ADR-017 dedup structural rather than a rule to remember; only `AddAsync` supplies one. Domain errors are pulled out of ProblemDetails before `EnsureSuccessStatusCode` so a page shows the business reason instead of "500". The same layer owns the list-page half, `DataGridListPageBase` (server-side paging through MudDataGrid `ServerData`, cancellation-token churn, a `LoadFailed` flag separating error-with-retry from genuinely empty, viewport-driven mobile cards, and list state persisted to URL, session storage and memory with the URL as source of truth), inherited by nineteen types. Trade-offs: no generated client means query-vocabulary drift fails the call rather than the build, retry budgets stack across hops so the shared per-hop count is pinned to one after full budgets everywhere turned a brownout into an up-to-16x storm, the policy is `static` and not configurable, and `ChildEntityServiceBase` calls get neither retry nor a key. | +| [095](095-soft-delete-unique-indexes.md) | Uniqueness under soft delete (filtered unique indexes) | The half ADR-005 never addressed: the query filter says a soft-deleted row does not exist, but a unique index still counts it, so deleting a record blocks re-creating it with an error whose conflicting row is invisible to the user. Making it a per-configuration `HasFilter` string would be the kind of rule that gets forgotten, so it is a convention instead: `SoftDeleteUniqueIndexConvention`, registered once in `ApplicationDbContext.ConfigureConventions` and therefore reaching every module, database and consumer repo through ADR-006's one-context-class-per-engine base, filters every unique, unfiltered, non-owned index on an `IAuditableEntity` at model finalizing, with nothing to opt into per entity. A hand-authored filter always wins and there is no flag: a configuration that wants uniqueness across deleted rows too opts out by declaring its own. Both the automatic path and the public `HasSoftDeleteFilter` opt-in run through one predicate builder, so they cannot disagree about which column carries the flag or about engine quoting (brackets for SQL Server, double quotes otherwise); Cosmos is a no-op on both. Trade-offs: adopting it moved consumer schema invisibly from the entity configuration (ADC's v1.120.0 migration dropped and recreated two unique indexes and needed an ADR-057 override, while Store's and Helpdesk's of the same sweep carried only the outbox columns, so only the generated migration says which), duplicates among deleted rows are now permanently legal (an `ignoreQueryFilters` report can see them, and any restore path has to handle the collision at undelete time rather than at delete time), and the predicate is not visible where the index is declared. | +| [096](096-best-effort-side-effects.md) | Best-effort side-effect contract | Five records each made post-commit follow-up work non-fatal for their own feature (ADRs 024, 026, 054, 076, 091) and each was right locally; none decided the **policy**, which left hand-rolled `catch (Exception)` blocks each picking their own severity, their own treatment of cancellation and their own decision to count nothing. `BestEffort.ExecuteAsync(operation, logger, action, cancellationToken)` in the Application layer fixes it once: the action is awaited rather than left as an orphan task racing the response, a failure produces exactly one `Warning` and exactly one `besteffort.dispatch.failed` increment tagged by a low-cardinality operation name on its own `MMCA.Common.BestEffort` meter (its own meter so an operator can drop or keep it independently of the RED metrics), a blank operation name or a null logger throws because the helper swallows the side effect's failures and never its caller's bugs, and an `OperationCanceledException` raised on the caller's own token is rethrown so an orderly shutdown stays a shutdown while an inner timeout is swallowed like any other failure. Post-commit callers pass `CancellationToken.None` on purpose, since the write has already committed. Adoption is seven production call sites across two apps. Two swallows deliberately stay hand-rolled and say so in code: Store's `AddVariantHandler`, whose loss is unrecoverable and logs at Error with the ids an operator needs to fix it by hand (evidence the work is not best-effort), and the framework's own `OutputCacheEvictionHandler`, which cannot reach Application from the API package without a layer-crossing reference. Recorded costs: nothing gates use of the helper (the only inventory is a search), the Warning carries the operation name and the exception and nothing else, the counter is failure-only and alerts on nothing, the meter name is a duplicated literal, and a swallow is still a loss (a cache entry heals on its TTL, a lost broadcast never replays). | ## Writing a new ADR diff --git a/docs/adr/001-manual-dto-mapping.html b/docs/adr/001-manual-dto-mapping.html index 0ab4c78..8eff241 100644 --- a/docs/adr/001-manual-dto-mapping.html +++ b/docs/adr/001-manual-dto-mapping.html @@ -96,7 +96,7 @@