to navigate Enter to open "…" all these words ANDOR to combine

Guides & specifications

MMCA.Common Capability Catalog

MMCA.Common framework, v1.187.0.

Everything an adopting team gets from the MMCA.Common framework: a .NET 10 package set for building a modular monolith on DDD, Clean Architecture, and CQRS, with a tested path to extract modules into microservices later. Every item below is grounded in a real shipped type, guide, CI job, or consumer call site (MMCA.ADC, MMCA.Store, MMCA.Helpdesk), verified against framework source at v1.187.0.

Figure What it counts
17 NuGet packages, lockstep versioned
124 architecture fitness methods (47 bases)
111 ADRs documenting every pattern
18 named middleware steps, order under test
23 device capability contracts, web and native
0 tokens needed to install (public nuget.org)

Package map

Seventeen packages, one version number. The layer rule is enforced at compile time inside the framework and at test time in every consumer: API/Grpc depend on Infrastructure, which depends on Application, then Domain, then Shared. UI and Grpc reference Shared only, so the UI runs in the browser and the transport stays pure.

Tier Package What it carries
Core MMCA.Common.Shared Result, errors, value objects, specifications, auth claim and permission primitives, resilience defaults, calendar and HTTP helpers. Safe for Blazor WASM.
Core MMCA.Common.Domain Entity hierarchy, aggregate helpers, invariants, domain and integration event bases, opt-in entity markers, PII and privacy contracts.
Core MMCA.Common.Application CQRS handlers and the decorator pipeline, module system, generic entity commands, validation rules, identity use cases, caching and messaging abstractions.
Core MMCA.Common.Infrastructure EF Core contexts for three engines, outbox and inbox, MassTransit broker adapters, scheduler, cache and lock implementations, audit trail, tenancy, encryption, identity stores, email, blob storage, push delivery.
Core MMCA.Common.API Controller bases, the named middleware pipeline, exception handlers, idempotency, rate limiting, authorization filters, JWT and JWKS and OIDC endpoints, session cookies, OpenAPI, CSV export.
Core MMCA.Common.Aspire Service defaults: OpenTelemetry, health checks, warm-up, Redis wiring, security headers and CSP, Serilog, Key Vault, data protection, gateway rate limiting and correlation, cost knobs.
Core MMCA.Common Metapackage bundling the six packages above as one reference (ADR-101).
Presentation MMCA.Common.Grpc Typed gRPC clients over service discovery, Result to and from gRPC status, server defaults with reflection.
Presentation MMCA.Common.UI Shared Blazor component layer: shell, pages, list-page bases, typed API clients, auth state, 23 device capability contracts, notifications, theming, localization.
Presentation MMCA.Common.UI.Web Blazor web head: server and WASM token handling, cookie sync, Blazor-aware CSP, form-factor detection, error page.
Presentation MMCA.Common.UI.Maui MAUI hybrid head across four targets (Android, iOS, Mac Catalyst, Windows): native capability implementations, secure token store, push token bridges, hardware back navigation, culture, crash handling. Built by dedicated Windows jobs (ADR-042).
Hosting MMCA.Common.Aspire.Hosting AppHost DSL: per-module data sources, RabbitMQ and Service Bus emulator brokers, JWKS discovery, MailDev, h2c health gates, E2E lifts.
Hosting MMCA.Common.Gateway YARP composition from configuration: cluster request profiles, health-check defaults, trace headers, per-route rate-limiter policies, forwarded headers. Its only runtime dependency is YARP.
Testing MMCA.Common.Testing Integration and cross-service fixtures, Service Bus emulator fixture, production-mode host factory, runtime conformance suites, JWT and feature-flag helpers, handler test base.
Testing MMCA.Common.Testing.Architecture 47 fitness-function bases over a 30-file ArchitectureRules library, parameterized by your architecture map. Folder-only organized (Rules/, Bases/ by topic) so the flat namespace consumers subclass never moves (ADR-015).
Testing MMCA.Common.Testing.E2E Playwright fixtures, Blazor-aware page helpers, page objects, eight inheritable workflow suites, axe-core profiles, Web Vitals budgets.
Testing MMCA.Common.Testing.UI bUnit component-test base with MudBlazor provider handles, mutable auth state, capturing HTTP doubles, markup snapshots (ADR-103).

Getting started and scaffolding

From zero to a green, warning-free, tested solution before writing any business logic.

  • One-command solution scaffold. dotnet new install MMCA.Templates then dotnet new mmca-app generates a complete 12-project solution (roughly 5,300 to 6,600 lines you never hand-type) with a named module and aggregate, ready for the six-step Getting Started guide.
  • Four templates at four grain sizes. mmca-app (solution), mmca-module (an 8-project module across all five layers plus tests and a migrations project), mmca-command and mmca-query (single vertical slice; handlers, validators, and mappers are convention-scanned, so no DI registration to write). A generated command slice arrives already passing the command-validator coverage rule.
  • A wire-up script, not printed instructions. Generated apps ship build/add-module.ps1, which runs the module template and then performs the wire-ups dotnet new cannot patch: solution entries, project references, identifier-alias link, architecture-map lines, error resources, AppHost database routing, the first EF migration, and the new module's integration event appended to the frozen wire-contract test so the freeze stays green.
  • The script refuses to half-apply. It detects the relational engine from the API host's own settings, cross-checks the existing migrations project, fails at preflight on a duplicate module name or an ambiguous architecture map, makes only anchored insertions so the diff shows added lines and nothing else, and is safe to rerun step by step.
  • Shape flags remove code instead of leaving dead weight. --flat, --no-status, --no-description, --no-owner, --child, --title, --event-verb, plus solution axes --database sqlserver|sqlite, --no-aspire, and -f|--framework-version to pin which framework release the generated app restores, so a scaffold is reproducible months later.
  • Green baseline on arrival. The generated solution builds warning-free under TreatWarningsAsErrors with five analyzers at error severity, and its domain, application, and architecture test suites pass with no database.
  • Credential-free install. All packages are on public nuget.org; no feed configuration or token. The MMCA.Common metapackage bundles the core six packages as one reference.
  • A deliberately low floor. --database sqlite --no-aspire gives two hosts, one database file, no Docker, no orchestrator, with nothing in the framework disabled that cannot be re-enabled by a config switch and a restart (not a migration). The Small Apps guide says when to flip each switch back on.
  • Source-mode development. A gitignored local.props flips every PackageReference to a ProjectReference against framework source, so you can develop the framework and your app side by side.
  • The generated app is gated, not just the seed. Templates are packed from MMCA.Helpdesk, the runnable reference app, and a dedicated template-smoke CI job packs, generates, builds, and tests a fresh app in package mode against a released framework, then sweeps the tree for residual reference-app tokens (ADR-065).
  • Templates release on their own cadence. MMCA.Templates ships on templates-vX.Y.Z tags through its own keyless trusted-publishing job (current line 1.10.0, MIT licensed), deliberately outside the framework's lockstep family so the framework version stays a parameter rather than a coupling.
  • Retrofit path for existing code. The Building by Hand guide walks the same solution phase by phase for teams adopting the framework inside an existing codebase; the two-module e-commerce sample shows the multi-module shape end to end.

Architectural core

The opinionated backbone: errors as values, one sealed pipeline, DDD building blocks, and module composition.

Errors as values

  • Result pattern end to end. Result / Result with nine ErrorType classifications (Validation, Invariant, NotFound, Conflict, Unauthorized, Forbidden, UnprocessableEntity, Failure, Unexpected), severity ranking, Combine, and a full combinator surface: map, bind, tap, Ensure, Match, OnFailure, implicit conversions, and Task> mirrors of each. Failures map to RFC 9457 ProblemDetails at the HTTP edge and to gRPC status on the wire, and ProblemDetailsResultReader reads them back into typed errors on the client, so pages branch instead of catch.
  • Results survive a cache. ResultJsonConverterFactory makes Result round-trippable through Redis, so a cached query result keeps its typed failure shape.
  • A reusable invariant library. CommonInvariants ships 22 Result-returning guards (string bounds, ids, enums, ranges, URLs, time zones, uniqueness, collection size, culture, theme) that module invariant classes compose with Result.Combine.

CQRS pipeline

  • Sealed decorator pipeline. Commands flow FeatureGate, Authorization, Logging, Caching, Validating, Timeout, Transactional, then the handler (queries the same six minus Transactional). Authorization sits outside Caching on purpose, so a denied query never reads or populates the cache; validation runs inside Caching because a cached entry was validated when produced. AddMmcaApplicationPipeline seals the composition: any framework registration helper called afterwards throws, and VerifyDecoratorPipeline() exposes the check to fitness tests for anything registered by hand. An opt-in profiling decorator pair is available via AddApplicationProfiling().
  • Behavior by marker interface. A handler opts into cross-cutting behavior declaratively: ITransactional, IQueryCacheable, ICacheInvalidating, IRequiresPermission, IFeatureGated, IHasTimeout. Transactional commands roll back on exceptions and on business failures alike; in-process domain events dispatch only after commit.
  • Contract markers with an inspector. Opt-in ICommand / IQuery on a request let CqrsContractInspector.FindContractMismatches fail a build when a handler's return type or kind drifts from what the request declared.
  • Convention-based registration. ScanModuleApplicationServices auto-registers command and query handlers, validators, DTO mappers and projectors, request mappers, update appliers, domain and integration event handlers, and a validator bridge for every command that embeds a request. The MmcaApplicationPipelineBuilder callback offers ScanModule, ScanModules, and Register for cross-service client wiring.
  • Guard-clause extensions. ICurrentUserService.RequireUserId(...) collapses the read-then-null-check block into one Result, and IReadRepository.GetByIdOrFailAsync returns a NotFound failure instead of a null.

Domain building blocks

  • DDD entity hierarchy. BaseEntity (identity equality with a transient-id rule so two unsaved rows are never equal), AuditableBaseEntity (audit fields, soft delete, row version), AuditableAggregateRootEntity (domain event collection plus five child helpers: SetItems with a validation hook, GetChildOrNotFound, RemoveChildOrNotFound, RestoreChild, and cascading DeleteChildren). Factory methods return Result.
  • Value object library. Seven ValueObject types: Money, Currency, Email, PhoneNumber, Address, DateRange, DateTimeRange, each with a Result-returning factory; Address, Email, and PhoneNumber carry separate invariant classes. EF value converters ship for Email, PhoneNumber, and Enumeration; Money maps as an owned type; Currency and Enumeration have JSON converters.
  • Smart enums are opt-in. Plain enums by default, Enumeration with its JSON and EF converters when a value needs behavior (ADR-104).
  • Opt-in entity markers. ITenantEntity, IAuditedEntity, IHasOrderingKey, IReactivatable, IAnonymizable, IErasableUser, IRowVersioned, IAggregateRoot: each is inert until the host enables the matching feature.
  • Declarative attributes. [UseDatabase] and [UseDataSource] route an entity to its database and engine, [IdValueGenerated] tells factories the database owns the key, [Navigation] lets the Application layer name include paths without EF, [ServiceContract] marks the wire surface of an extractable service, [EventName] pins a wire name, [Pii] marks personal data.
  • Composable specifications. Specification / QuerySpecification with criteria, ordering, includes, paging, tracking control, and .And()/.Or()/.Not() composition. CrossSourceSpecification.BuildAsync resolves a principal's keys in its own database and returns a portable key IN (...) specification, so a predicate that would need a cross-database join stays translatable.
  • Manual, compile-checked mapping. Mapperly DTO mappers and projectors behind IEntityDTOMapper / IEntityDTOProjector: no runtime reflection mapping surprises (ADR-001).

Module composition

  • Module system. Modules implement IModule (a leaf module is a name plus Register); ModuleLoader discovers them by reflection and registers them in topological (Kahn) order. Seeding is a separate IModuleSeeder contract invoked in the same order. A module disabled by ModulesSettings gets stub registrations so cross-module interfaces still resolve, and RemoteDependencies declares that a disabled dependency is satisfied out of process.
  • Identifier type aliases. Entity ID types are solution-wide global using aliases linked into every project, so switching an ID type is a one-line change.
  • Feature by folder, enforced. The aggregate names the first folder level in Domain, Application, and Shared; UI, API, and Infrastructure put a technical root first with the aggregate beneath it; no folder holds more than twelve direct code files, and namespaces follow folders. The framework's own packages are organized the same way (Messaging/, Notifications/Push/, Capabilities/Geo/), and FolderWidthTestsBase makes the cap a merge gate in every consumer (ADR-109).
  • Shared utilities you would otherwise write. KeyedSemaphoreStripe (bounded per-key locking), IcsCalendarBuilder (RFC 5545 export), ConcurrencyETag, IdempotencyHeaders, ModuleNameConventions, ClaimsPrincipalExtensions, and one shared set of HTTP, gRPC, and broker resilience defaults.

Monolith now, services later

The framework's central promise, and it is a tested property, not a slogan.

  • Host-only extraction. Turning a module into its own service changes hosting code only; Domain, Application, Shared, Infrastructure, and API code stay untouched. You add a service host (AddModuleHost boots one module per process), a .Contracts proto project, a gateway route, broker transport, and JWKS auth.
  • Database per module from day one. Even inside the monolith each module owns its database with its own outbox and inbox tables, and relationships that cross a database boundary degrade automatically (FK constraint and navigation removed, scalar column and index kept), so extraction costs no data migration rework.
  • YARP gateway as a package. AddMmcaGateway() composes four things from configuration: cluster request profiles layered per property (defaults, then per-cluster overrides), health-check defaults (passive on by default, active probing configured and one flag away), trace headers that strip any client-supplied value before stamping the gateway's own, and named per-route rate-limiter policies. UseCommonForwardedHeaders() ships alongside. The package's only runtime dependency is YARP; the host still loads its route table and maps the proxy.
  • gRPC cross-service calls with Result over the wire. AddTypedGrpcClient gives service discovery over h2c, JWT forwarding, the standard resilience pipeline, and a pinned SocketsHttpHandler so HTTP/2 negotiation and connection rollover survive the global client defaults. AddGrpcServiceDefaults() adds the Result interceptor, turns detailed errors off, and enables reflection. ResultGrpcExtensions maps all nine error types in both directions, so a caller branches on Result instead of catching RpcException. Any *.Contracts project auto-compiles its protos. The hand-written adapter over the generated client is the consuming module's Anti-Corruption Layer, the one place the peer's wire model is translated into the module's own contract (ADR-007), and lifting a module out follows the Strangler Fig route: stand the service host up beside the monolith, move traffic through the typed client, retire the in-process path last (ADR-008).
  • Transport-agnostic eventing. The same IEventBus publish call works in-process (monolith) or over RabbitMQ / Azure Service Bus (extracted), selected purely by MessageBus:Provider; the outbox turns itself on when a broker appears (ADR-100).
  • Broker parity locally. The AppHost can provision the official Azure Service Bus emulator, so a development stack exercises the production transport rather than a stand-in.
  • Cross-service trust without shared secrets. WithJwksDiscovery(identity, gateway) routes JWT metadata discovery through the gateway (extracted services listen HTTP/2-only on cleartext), GetRequiredJwtAuthority() fails at startup if the authority was never injected, and OIDC discovery lets each service derive the issuer instead of pinning it.
  • Extraction readiness is enforced. MicroserviceExtractionTestsBase fails the build if MassTransit, Grpc, or Protobuf types reach Domain, Application, or Shared; ServiceContractPurityTestsBase keeps a [ServiceContract] free of the producer's internals; ContractImplementationTestsBase keeps implementations non-public; ProtoContractTestsBase diffs the live proto against a committed snapshot.
  • Guided by the docs. The guides tell you to extract on an observable constraint, not on principle, and walk the full extraction phase step by step (ADRs 007 and 008).

Data and persistence

EF Core with the cross-cutting concerns every production system eventually rebuilds, already built.

Routing and contexts

  • Multi-database routing. Every entity resolves to a DataSourceKey (engine, name): the engine from its configuration base class, the database from [UseDatabase], the module namespace, or Default. One sealed context class per engine (SQLServerDbContext, CosmosDbContext, SqliteDbContext) over the abstract ApplicationDbContext, one instance per database, with EF's model cache keyed per source (ADR-006).
  • Logical-to-physical collapse. Logical names that share a connection string collapse onto one physical source, and a framework table asking for an engine the host never configured gets the configured one, so a single-database SQLite host runs the whole framework unchanged.
  • Cross-source navigation. INavigationPopulator batch-loads navigations that span physical databases (ADR-002); transactions are per source, best-effort sequential, with cross-source consistency left to the outbox.
  • Repository, unit of work, and narrow read interfaces. IRepository / IUnitOfWork with transaction control, narrowed IEntityReader / IEntityQuerier for read paths, an ExecuteUpdate builder for set-based writes, and a repository factory that wraps every repository in MiniProfiler timing when profiling is on.
  • Schema lifecycle on one interface. IDbContextFactory exposes MigrateAsync, HasPendingMigrationsAsync, EnsureCreatedAsync, and RequestIdentityInsert across every physical source; InitializeDatabaseAsync drives per-source migration and module seeding from a DatabaseInitStrategy.

Cross-cutting behavior

  • Soft delete done right. IsDeleted flag, a named SoftDelete query filter that can be dropped independently of the Tenant filter, filtered unique indexes so a deleted row does not block re-creating the same value (ADR-095), HasSoftDeleteFilter() for hand-authored indexes, RestoreChild for reversible deletes, and cascade-to-children enforced by a fitness rule.
  • Automatic audit stamping and concurrency. CreatedOn/By, LastModifiedOn/By, DeletedOn/By stamped by a save interceptor, and RowVersion configured as the concurrency token on every auditable entity by the base context, so no entity configuration opts in.
  • Optimistic concurrency via ETag / If-Match. RowVersion round-trips as a weak ETag; [SupportsIfMatch] yields 428 / 412 semantics; a convention test keeps every mutating endpoint covered.
  • Field-level audit trail (opt-in). AddAuditTrail() records per-field change history in the same transaction as the data, redacts [Pii] values at capture, stamps each row with trace id and tenant, exposes IAuditTrailReader, and sweeps on a retention window through the scheduler.
  • Field-level encryption at rest. EncryptedStringConverter encrypts designated columns with AES-256-GCM in a versioned envelope over a key ring: writes use the current key version, reads resolve theirs from the stored envelope, so rotation is zero-downtime, and an unregistered version fails loudly. GenerateKey() produces a conforming key.
  • Multi-tenancy in two isolation modes. Shared schema via the tenant query filter and a stamping interceptor with cross-tenant write protection, or database-per-tenant via a per-tenant per-source connection override. Three ordered resolution strategies (claim, header, host), RequireTenant, excluded path prefixes, tenant-scoped cache keys, a startup validator that rejects an override naming a nonexistent source, and background sweeps that visit every per-tenant copy of a source.
  • One transaction contract, ambiguity reported, never retried. ExecuteInTransactionAsync is re-entrant (an inner call joins the ambient transaction), runs the whole delegate under EF's execution strategy with a change-tracker reset per attempt, rolls back on a failed Result exactly as on a throw, and never retries the commit: a commit whose outcome is unknowable escapes as TransactionCommitAmbiguousException naming each physical source as committed, ambiguous, or rolled back (ADR-107). IUniqueConstraintViolationDetector turns a lost insert race into a typed conflict instead of a 500.

Jobs, paging, migrations

  • Persistent cron scheduler. IScheduledJob with claim-lease execution so an occurrence runs exactly once across replicas, per-job cron overrides from configuration (Scheduler:Jobs:{Name}:Cron), a run-history row per job (outcome, duration, truncated error), and three metrics.
  • A sweep base for your own jobs. PeriodicBackgroundService gives an enablement gate, startup delay, a fixed-interval loop that survives a failing cycle, and TimeProvider-driven waits so the loop is testable with a fake clock.
  • Keyset and offset pagination. Opaque cursor encoding and a keyset query builder for stable deep paging, classic offset paging with X-Pagination metadata, a 1,000-row ceiling on any unbounded read inside the query pipeline, and Id appended as a total-order tie-break on every page.
  • Migration ergonomics. Design-time factories for SQL Server and SQLite that never open a connection (dotnet ef ... -- --datasource Name), per-service migrations assemblies, a startup sole-migrator pattern, expand/contract schema evolution enforced as a CI gate, and the consumer canary applying MMCA.Helpdesk's real migrations to an ephemeral SQL Server on every framework PR.
  • Seeding as a shipped base. IDbSeeder handles int or Guid seed identifiers, and IdentityModuleDbSeederBase seeds development accounts (normalize, skip existing, hash, create, save) behind two hooks.
  • Schema ships with the framework. Outbox, inbox, audit trail, scheduler, notification, and refresh-session tables arrive as scaffolded consumer migrations when you enable the feature or bump the framework. One Persistence:CommandTimeoutSeconds knob applies to every relational source; CosmosIntIdValueGenerator covers the engine without identity columns.

Messaging and eventing

Reliable events with two deliberately distinct paths: in-process domain events and durable integration events.

  • Transactional outbox. The save interceptor writes every captured domain event to OutboxMessages in the same transaction as the data; local events dispatch in-process after commit and their rows are stamped processed, while integration event rows stay pending for OutboxProcessor. The processor drains on signal or a smart wait, batches, retries with jittered exponential backoff capped at the claim lease, dead-letters loudly, claims rows under a lease so replicas scale out safely, delivers in order per key for events implementing IHasOrderingKey, and captures W3C trace context so a consumer's work correlates back to the request that caused it (ADR-003).
  • An operator surface, not a SQL script. IOutboxAdministration lists and replays dead letters and counts pending rows across every source the host owns; replay resets the retry count and lease but keeps the last error and original timestamp.
  • Failure isolation. A circuit breaker guards the broker publish call only, so a broker outage stops publishing without stopping retry-state persistence. Five instruments (outbox.processed.count, dead_letter.count, dispatch.lag, pending.depth, oldest_pending.age) plus broker fault and breaker counters report health.
  • Retention as a privacy control. Processed rows purge after Outbox:RetentionDays (payloads can carry personal data); dead-lettered rows keep a separate, wider window so they stay long enough to diagnose and replay. Eleven outbox knobs in all.
  • Consumer-side inbox. EfInboxStore dedups broker redeliveries inside the handler's own transaction, giving effectively-once handling on top of at-least-once delivery.
  • Broker choice by config. MassTransit (pinned v8) behind IMessageBus: InProcess, RabbitMQ, or Azure Service Bus by MessageBus:Provider. Two retry tiers: in-process exponential retry (5 attempts, 1s to 30s) then broker-scheduled delayed redelivery (60s, 600s, 3600s), unconditional on Service Bus and plugin-gated on RabbitMQ. EndpointPrefix namespaces queues on a shared broker; a broker paired with EnableOutbox=false fails at startup; a host running with the outbox or inbox off logs one startup notice.
  • Schema-versioned events with upcasting. BaseIntegrationEvent carries a SchemaVersion; IEventUpcaster registrations migrate old payloads on consumption. EventUpcasterRegistry rejects duplicate sources, self-maps, and cycles at construction and restamps message identity after each hop so inbox dedup keeps working; a startup validator and a contract fitness test freeze the wire shape.
  • Two event paths, one aggregate API. AddDomainEvent on the aggregate feeds both in-process dispatch (post-commit, compiled-delegate dispatcher) and outbox-backed integration events. Three consumer verbs: RegisterIntegrationEventConsumer (with an automatic fault consumer that turns an exhausted retry into one structured error plus a metric), RegisterUpcastedIntegrationEventConsumer for draining a retired contract, and RegisterOutputCacheEvictionConsumer.
  • Handler bases that keep the retry promise honest. SafeDomainEventHandler logs a failure with full context and then lets it propagate so the outbox actually redelivers; ScopedIntegrationEventHandlerBase gives a cross-module handler its own DI scope.
  • Saga support. Compensation plus a reconciliation backstop pattern for cross-module workflows (used by Store's order payment flow).
  • Stable wire names. [EventName] decouples event class names from wire contracts for rows written after the attribute is applied; renaming with pending rows is a documented drain-then-rename move.
  • Best-effort side effects. BestEffort.ExecuteAsync standardizes swallow-and-log for side effects that must never fail the main operation, counts each swallowed failure on its own meter, and never swallows the caller's own cancellation (ADR-096).
  • Shipped event shapes. EntityChangedEvent with DomainEntityState, and OutputCacheEvictionRequested as a framework-owned integration event.

HTTP API surface

Controllers and middleware that erase most CRUD endpoint code and keep the pipeline order under test.

Read side

  • Five generic read endpoints for free. EntityControllerBase gives list, paged list, lookup, by-id, and CSV export, with dynamic filters bound from ?filters[Name].operator=contains&filters[Name].value=... (typed strategies chosen by the leaf property of a dotted path, extensible via RegisterStrategy), sparse fieldsets, sorting, and pagination headers. Helpdesk's CSV export endpoint is zero lines of module code: the controller base supplies it.
  • One row-scoping hook governs all five. GetReadSpecificationAsync is ANDed with the caller's filters on list, paged, lookup, by-id, and export alike, and a row it excludes returns 404 rather than 403, so a scoped read is never an existence oracle and export cannot drift wider than the list it mirrors.
  • ETags from the generic path. GetByIdAsync publishes the DTO's RowVersion as a weak ETag; a protected SetConcurrencyETag lets hand-written reads emit the identical header.
  • CSV export with a stated ceiling. Every export sends X-Export-Row-Limit up front, page-loops the query service, appends an in-body truncation record if the ceiling is hit, drops binary and collection columns, and uses a hand-written RFC 4180 writer with invariant formatting, so no CSV library is added to every consumer.

Write side

  • Generic write endpoints and handlers. AggregateRootEntityControllerBase ships idempotent POST and DELETE; CrudEntityControllerBase adds PUT carrying both [Idempotent] and [SupportsIfMatch]. AddEntityCrud() registers the create, update, and delete commands and the update validator bridge; AddEntityUpdateVerb and AddEntityUpdate cover derived update commands that carry route-derived ids or server-decided flags (ADR-099).
  • Handler bases for richer writes. MutateEntityHandlerCore supplies the load-mutate-save workflow with three subclasses for the three return shapes, plus CreateEntityHandlerBase and the add-child / remove-child pair for aggregate children. MutationContext carries typed values from load to post-save hooks, and SkipSave() is an idempotent no-op signal.
  • Validators written once, against the request. CommandRequestValidator bridges a command to its embedded request's rules, registered automatically by the module scan. Seventeen reusable FluentValidation rule classes (required and optional strings, email, absolute URL with a javascript: / data: guard, positive and non-negative numbers, required and optional ids, password and strong password, six address fields) each accept an optional machine-readable error code.
  • HTTP idempotency. [Idempotent] caches responses by Idempotency-Key for 24 hours under a composite SHA-256 key (subject, method, route, client key), holds a distributed lock across the execute-then-store window, answers an in-flight duplicate with 409, and marks replays with X-Idempotent-Replay (ADR-017). [NonIdempotent(justification)] is the required marker for the rest, and a fitness rule fails any POST carrying neither.

Pipeline and errors

  • Eighteen named middleware steps, order under test. UseCommonMiddlewarePipeline() applies ExceptionHandler, CorrelationId, RequestLocalization, PreForwardedCapture, ForwardedHeaders, HttpsRedirection, ResponseCompression, Routing, Cors, Authentication, TenantResolution, RateLimiting, SoftDeletedUserFilter, Authorization, OutputCache, JwksEndpoint, OidcDiscoveryEndpoint, Controllers. A second overload hands you MiddlewarePipelineBuilder to insert, replace, or remove steps by name, and Build() rechecks four load-bearing adjacency invariants before anything is applied. A shipped test base pins the order in every consumer.
  • Error mapping as a system. Five exception handlers registered most-specific first (cancellation, domain, DB update, validation, global) plus a requestId extension on every ProblemDetails; UnhandledResultFailureFilter rewrites a failing Result that an action returned inside Ok(...) into the correct ProblemDetails status instead of a 200.
  • Service-side rate limiting. AddCommonRateLimiting binds an eight-knob RateLimiting section: permit and queue limits, per-user and global limits, fixed or sliding window, and Distributed, which moves counters onto Redis and degrades to per-instance when no multiplexer exists. A dedicated auth-ip policy throttles anonymous sign-in attempts per IP and never depends on Redis. Probes, .well-known, and gRPC traffic are exempt.
  • API versioning, OpenAPI, Scalar UI. AddCommonApiVersioning, AddCommonOpenApi (one document per version, with a descriptor backfill so URL-segment versioning and tenant tokens cannot 500 the document), and a versioned /service-info endpoint, all covered by shipped contract test bases. OpenAPI and Scalar are no-ops in Production and Scalar's assets are bundled, no CDN.
  • Feature flags at endpoint and handler level. Feature management integration with user-targeting context, a disabled-feature handler, and IFeatureGated short-circuiting in the CQRS pipeline.
  • Localized error codes and culture. AddErrorResources() per module returns localized, coded errors the UI displays directly; UseCommonRequestLocalization() and MapCultureEndpoint() (GET /culture/set, allow-listed cultures, Development-only pseudo-locale) ship as extensions.
  • Module-aware plumbing. Controllers of config-disabled modules are removed from MVC's application parts so no live endpoint is left behind; AddModuleHealthChecks emits one module-{Name} check per module, Degraded when disabled.
  • Ready-to-mount extras. AddNotificationControllers() adds three concrete controllers (send and history, inbox, devices); MapNotificationHub() maps the SignalR hub only when push is enabled; MapAppAssociationEndpoints serves Apple app-site-association and Android asset-links for deep links; AddMiniProfilerIfEnabled mounts /profiler behind a settings flag.

Identity, auth, and authorization

A full identity building-block set you compose when you want it; a fresh app runs issuer-less until then.

Tokens and trust

  • RS256 + JWKS + OIDC discovery across services. TokenService signs with RS256 by default (HS256 selectable by configuration for a single host); MapJwksEndpoint and MapOidcDiscoveryEndpoint serve /.well-known/jwks.json and /.well-known/openid-configuration; every service validates through AddForwardedJwtBearer with the algorithm pinned to RS256 as defense against algorithm confusion. The issuer is derived from discovery, so the internal authority and the public issuer can differ.
  • Insecure metadata is legal but loud. RequireHttpsMetadata resolves argument, then configuration, then true outside Development; a resolved false outside Development installs a startup filter that logs one warning naming the key rather than failing or staying silent.
  • SignalR authenticates off the query string. Both bearer registrations lift access_token for /hubs paths, because WebSocket connections cannot send an Authorization header.
  • JWT plus rotating refresh sessions. Multi-device sessions store only a SHA-256 digest, record a rotation chain, and treat a replayed rotated token as reuse that revokes the whole live family; TryRotateAsync makes the rotation an atomic database claim so two simultaneous refreshes cannot both mint a successor. A per-user cap (default 10) evicts the oldest session, the table lives in the Identity database only, and spent rows are hard-deleted on a retention window (ADR-097).
  • Per-device session management. GetSessionsAsync, RevokeSessionByIdAsync, and RevokeAllSessionsAsync behind an additive sid claim; the controller base exposes my-sessions and revoke-by-session, and the UI ships a sessions page with per-device and account-wide revoke paths.

Credentials

  • Password security. PBKDF2-HMAC-SHA512 with 600,000 iterations, a 32-byte salt, and constant-time verification as the only supported algorithm (ADR-102), pinned by known-answer tests.
  • Brute-force protection is five configured numbers. ILoginProtectionService applies per-email lockout with exponential backoff and a per-IP registration rate limit on cache-backed atomic counters keyed on normalized identity, so capitalization cannot defeat the backoff.
  • Reset tokens are hashed, capped, and throttled. IPasswordResetTokenService keeps one active token per address, stores only its digest, discards it after a bounded number of wrong guesses, and throttles requests per email per window, all on cache TTL with no schema and no sweeper (ADR-091).
  • External OAuth. Google, GitHub, and Apple via AddExternalAuthProviders, each gated independently on its ClientId, with a ten-minute ExternalLogin cookie carrying the external principal to completion, GitHub's email scope requested explicitly, Apple's ES256 client secret minted from key material rather than a static secret, and a POST exchange action for the native mobile callback.

Authorization

  • Permission-based authorization. Endpoints declare capabilities with [HasPermission] against a permission registry; policies materialize on first use, so nothing is pre-registered per role. Modules declare grants additively with AddPermissions, and a host that never declares any gets a registry that denies everything and logs once naming the fix. RoleValue gives case-insensitive role equality against a per-app known set.
  • Resource-ownership authorization. OwnerOrAdminFilter reads the owner id from the route or bound arguments, parses invariantly, and denies when it cannot resolve one; [AllowMissingOwner] is the explicit opt-out. On the handler side, UserOwnershipRule.CheckOwnership with IUserOwnedRequest / IUserScopedRequest markers is what every shipped account handler uses.
  • Session-cookie (BFF) auth for Blazor SSR. MapSessionCookieEndpoints() maps the cookie set and clear endpoints plus a same-origin validate-or-refresh call that hydrates the browser's in-memory access token while the refresh token stays server-side; it rejects cross-site fetches on top of SameSite, and a single-flight refresher collapses a thundering herd so a token is never double-rotated.
  • Soft-deleted-user revocation. A pipeline step rejects requests from confirmed deleted accounts with 401, backed by a 30-second cache marker and a database validator fallback; on a cache or validator error it logs and lets the request through (fail open, by design). The shared erasure workflow writes the marker itself right after commit and before any app-specific tail runs, so every app gets the identical token-revocation window without hand-rolling the write (ADR-047).

Account use cases and UI

  • Reusable account use cases. AuthenticationServiceBase implements login, register, refresh, revoke, sessions, and external login; seven handler bases cover change password, change and get preferences, forgot and reset password, delete account, and data export; three controller bases expose login, register, refresh, revoke, my-sessions, revoke-by-session, password, preferences, forgot-password, and reset-password. An Identity module becomes thin subclassing.
  • Auth UI included. Login, register, forgot and reset password, OAuth completion, and session management pages ship in the UI package, every return URL passes through an open-redirect guard, and client-side password and URL validation attributes mirror the server rules (section 10).

Caching, resilience, and performance

Server side

  • Declarative handler caching. A query implements IQueryCacheable, its invalidating commands implement ICacheInvalidating, and the pipeline does the rest; tenant-scoped keys are automatic. On a miss, a per-key striped lock with a double check means exactly one request populates a hot key (stampede protection), bounded by Cache:PopulateLockTimeout; failed results are never cached; hits and misses are metered.
  • Swappable cache substrate. Memory, distributed Redis, or opt-in HybridCache behind one ICacheService, with Cache:KeyPrefix namespacing every key on a shared Redis, a 30-second default TTL, an L1 ceiling for the two-level cache, a disjoint hc: keyspace so the two implementations never meet at one key, and RemoveByPrefixAsync and atomic IncrementAsync beyond get and set. Every call is fail-open: a cache outage degrades to uncached reads.
  • Distributed locking ships with the cache. IDistributedLock is one non-reentrant, TTL-bounded, best-effort contract (TryAcquireAsync(key, ttl, wait) with owner-scoped release): Redis SET-NX-PX with Lua compare-and-delete when a multiplexer exists, otherwise a warn-once in-process fallback, registered by the same AddCaching call. It guards windows like the idempotency execute-then-store gap; anything the database can enforce keeps its claim lease instead (ADR-108).
  • Readiness-safe Redis wiring. AddRedisCaching() and AddRedisOutputCaching() register Aspire's Redis integrations with their untagged health checks switched off (they issue an admin-class command that Azure Managed Redis refuses) and add a PING-only redis check tagged optional; both are no-ops without a connection string (ADR-025).
  • Output caching at the edge. AddPublicEndpointPolicy(name, expiration, bypassRoles, tags) varies by every query key, skips lookup and storage for any bypass role, and refuses to store a response that sets a cookie or is not a 200 (ADR-040). EvictTagsAsync and best-effort TryEvictTagsAsync evict multiple tags at once; cross-host eviction is one DI call plus one bus registration, driven by the OutputCacheEvictionRequested event.
  • Warm-up before ready. IWarmupTask registrations run before /health/ready reports healthy, so a cold instance never receives traffic; the gate opens whether or not the tasks succeed, each task is capped at 120 seconds, an OIDC-metadata warm-up is built in, and SelfHttpWarmupTaskBase replays your hot read paths against the host's own port.
  • Standard resilience everywhere. One shared set of numbers (30s per attempt, 60s breaker window, 90s total, exactly one retry per hop so a brownout cannot become a request storm) for HTTP, gRPC, and broker; every outbound client also gets a pooled connection lifetime, idle timeout, and TCP keep-alive tuned so a low-traffic replica stays on idle billing.

Client side

  • Client-side staleness policy. IUiReadCache is a per-circuit read-through cache keyed on path plus query (the same key shape as the server output cache), with a 60-second default, longest-prefix per-route TTLs, prefix invalidation after a write, and a full clear on sign-out; opt in per service through the EntityServiceBase constructor.
  • Typed clients retry with a constant idempotency key. AuthenticatedServiceBase retries transport failures, 5xx, 408, and 429 three times with 2s, 4s, 8s backoff plus jitter, disposing each retried response, and HttpResultExecutor turns transport failures and timeouts into typed Result errors.
  • Offline and stale-load guards. OfflineFirstPageSnapshot serves a list page's first page from device storage when offline; LatestLoadGuard cancels the previous load on a route-parameter change so a slow response can never overwrite the entity on screen; UseAuthenticatedNoStore() keeps signed-in pages out of the back-forward cache while public pages stay eligible.

Gates and cost

  • Performance as a gate. BenchmarkDotNet suites for specification evaluation and the query pipeline, a committed baseline enforced by perfgate (allocation ceilings in bytes, machine-independent latency ratios, and a missing benchmark counts as a violation), and Core Web Vitals budgets enforced as E2E assertions (ADR-092).
  • Cost knobs baked in. Four telemetry knobs: Telemetry:FilterProbeTelemetry (on by default, keeps probe traces and their child spans out of export), TracesSampleRatio, and DisableHttpClientMetrics / DisableRuntimeMetrics, which drop whole meter families with views so they hold even under the Azure Monitor distro. Outbox poll interval and every retention window are configuration; the FinOps guide lists the levers (rubric section 31).

UI: Blazor web, desktop, and mobile

One shared component layer serving Blazor Server, WASM, and MAUI hybrid heads on Android, iOS, Mac Catalyst, and Windows.

  • Module-composed application shell. AddUIModule() scans a module's typed services and registers its IUIModule descriptor; the shared router and layout consume nav items, app-bar and layout components, and routable assemblies from every module. Twelve pages ship in the shared layer: home, login, register, forgot and reset password, OAuth completion, sessions, notification inbox (with a typed deep-link route), notification list and send, not-found, and forbidden; the web head adds the error page.
  • List pages nearly for free. DataGridListPageBase wires MudBlazor's grid to server-side paging, filtering, and sorting in three layouts: paged desktop grid, opt-in virtualized grid, and mobile infinite scroll. Page, size, sort, density, filters, and scroll position persist per route in session storage and round-trip through the URL query string, so a shared link reopens on the same rows.
  • Result-shaped API clients. EntityServiceBase typed clients return Result for every outcome, and ResultUiExtensions adds localized error lists, NotifyOnFailure straight to a toast, and IsNotFound / IsUnauthorized predicates so a page branches on classification instead of catching.
  • Vendor-neutral toast and dialog facades. IToastService and IAppDialogService are the only types that name MudBlazor's snackbar and dialog services, so the component library stays replaceable and a bUnit test records toasts without a rendered host.
  • Full auth UI stack. Authentication state provider, a two-layer token store (raw persistence beneath a freshness-checking layer, so a refresh can never re-enter the acquisition that started it) with per-head implementations for WASM, server cookie, and MAUI secure storage, token refreshers, session-cookie sync, ReturnUrlProtector against open redirects, a drop-in BiometricGate, and a native OAuth broker for the system-browser round trip.
  • Client-side validation parity. PasswordComplexityAttribute and AbsoluteUrlAttribute mirror the server rules so an EditForm gives the verdict the API would, with every message resolved through the page's localizer.
  • Device capability abstraction. Twenty-three contracts registered by one call: share, clipboard, haptics, geolocation, geocoding, map navigation, external links, biometrics, barcode scanning, media picker, local notifications, push registration, push device token, connectivity, battery, preferences, speech-to-text, text-to-speech, accessibility announcer, screenshots, deep links, external auth broker, and a local cache store, grouped into nine families (accessibility, auth, device status, device storage, geo, interop, media, navigation, notifications) where each family holds the contract, its browser implementation, and its null fallback together. Browser implementations override eight, MAUI overrides twenty-two, and null fallbacks cover the rest, so one component tree runs on web and device.
  • Push notifications end to end. SignalR hub for live in-app delivery with scoped channels (JoinChannelAsync / LeaveChannelAsync reference-counted so one component leaving cannot disconnect another), a configurable notification bell, an inbox with unread counts, native push via FCM and APNs through Azure Notification Hubs with token-rotation handling (ADR-044), and a singleton deep-link dispatcher that buffers cold-start taps until the router is alive.
  • Navigation that behaves like an app. Real history.back() with route fallback, the Android hardware back and iOS swipe routed into the WebView history stack, IPublicLinkBuilder so shared links never point at the WebView's internal origin, and IFormFactor per head. The shared layout clips horizontal overflow at every width, so an off-screen drawer cannot widen the mobile layout viewport and push centered snackbars and dialogs to the screen edge.
  • Theming and dark mode. Shared theme with brand color tokens, OS-preference default, a toggle component, the choice persisted per user through the API, and mirrored into the native MAUI app theme.
  • Internationalization. English and Spanish resources at string-for-string parity (182 shared strings plus 145 MudBlazor chrome strings), a culture switcher with cookie forwarding, a document-language component, an in-process culture path for MAUI heads that persists the choice in device preferences, pseudo-localization for testing, and translation completeness enforced as a fitness gate.
  • MAUI hosting one-liners. UseMauiDeviceCapabilities() registers the native implementations and culture path, UseMmcaMauiErrorHandling() installs last-chance unhandled and unobserved exception handlers with an optional crash-reporter callback, and AddMauiPushDeviceTokenProvider() selects FCM or APNs by target framework, inert until push credentials exist.
  • Utility components. Error summary, delete confirmation, unsaved-changes guard, empty, loading, and error states, page header (guaranteeing the axe-required h1), QR code display, file download button with a sanitized file name, share button, offline banner, infinite-scroll sentinel, mobile card list, external link, lazy JS module loader, and a bfcache-restore refetch scope.
  • Accessibility built in. WCAG 2.1 AA is a shipped, CI-enforced test contract across three browser engines, not an aspiration (section 12).

Hosting, orchestration, and operations

Aspire-first local orchestration and one-line production plumbing.

AppHost and service defaults

  • AppHost DSL. Thirteen extensions: WithSQLServerDataSource, WithCosmosDataSource, WithSqliteDataSource (per-module databases), AddMessageBroker and AddServiceBusEmulatorBroker with matching WithBroker overloads, WithJwksDiscovery, AddMailDev (local SMTP sink with a web inbox on fixed ports), WithH2cHealthCheck, and three E2E helpers: WithE2eRsaKeys, WithE2eRegistrationThrottleLift, WithE2eGatewayRateLimitLift, all triggered by one environment variable absent locally and in production (ADR-098).
  • Service defaults in one call. AddServiceDefaults() wires OpenTelemetry, service discovery, standard resilience, default health checks, and warm-up readiness; MapDefaultEndpoints() maps /health, /alive, and /health/ready. Three tags decide what each endpoint consults: live, ready, and optional, the last so a degradable dependency shows on /health without turning every replica unready at once.
  • Infrastructure health from configuration alone. AddInfrastructureHealthChecks(requireDatabase) registers one check per declared relational database plus optional-tagged Redis and RabbitMQ checks, and throws at startup when a host that requires a database has none configured.
  • h2c endpoint profiles with a probe listener. ConfigureEndpointsWithHealthProbe configures cleartext HTTP/2 (for gRPC behind a gateway) or mixed HTTP/1+2 profiles, and adds a dedicated HTTP/1.1 listener when HealthProbe:Port is set, because platform probes speak HTTP/1.1 and an HTTP/2-only endpoint answers HTTP_1_1_REQUIRED. WithH2cHealthCheck gates Aspire WaitFor edges on a prior-knowledge probe and defaults to /alive, since gating startup on readiness can deadlock the graph.
  • Module hosting. AddModuleHost binds and validates application and module settings, builds the loader, and hands back a context the host places inside its own registration order; the same module runs unchanged inside the monolith.

Gateway operations

  • Downstream probes that negotiate their own protocol. AddGatewayDownstreamHealthChecks registers one readiness check per fronted service, resolved through service discovery, trying h2c and falling back to HTTP/1.1 inside the same poll and latching the answer for the process lifetime.
  • Edge rate limiting distinct from the service-side limiter. A per-client-IP fixed window chained with a replica-wide concurrency ceiling, both answering 429 with no queue, with probes and .well-known always exempt so a spike cannot fail a liveness probe into a restart, an unresolvable IP failing open rather than collapsing into one shared bucket, host-specific BypassPathPrefixes, and a secret-gated, constant-time synthetic-traffic bypass so a scheduled capacity proof measures the system rather than the limiter.
  • Correlation and CORS at the edge. UseGatewayCorrelation() stamps X-Correlation-ID (preferring the W3C trace id) so the service-side middleware adopts the same id, with no dependency beyond the request delegate; AddCommonGatewayCors gives the edge its deliberately looser two-tier policy.

Observability and production plumbing

  • Observability with framework meters. Seven meters (CQRS, outbox, broker, scheduler, idempotency, output cache, best-effort) and two trace sources, exported to OTLP and Azure Monitor simultaneously when both are configured; outbox poll spans and probe telemetry are filtered from export by processors, and correlation IDs propagate from gateway to service.
  • Serilog that does not silence Application Insights. AddCommonSerilog registers Serilog as one provider rather than replacing the logger factory, so application log lines still reach the OpenTelemetry export; a bootstrap logger covers startup diagnostics.
  • Secrets and keys. AddCommonKeyVaultConfiguration layers a vault over host configuration under managed identity, gated on KeyVault:Uri so local hosts take no Azure dependency, with a reload interval so a rotated secret reaches the process without a redeploy. AddCommonDataProtection persists the key ring to blob storage with an optional, deliberately decoupled Key Vault encryption step, so a lagging role assignment cannot break sign-in.
  • Storage and email. AddAzureBlobFileStorage behind IFileStorage, and SmtpEmailSender behind IEmailSender, both in the Infrastructure package.
  • Security headers and CSP. One-call security response headers with a complete default policy sized for Blazor and MudBlazor, a per-request {nonce} path exposed through CspNonce.Get as the supported way off unsafe-inline, a Report-Only mode for rolling a tightened policy out in observation first, configurable HSTS, frame, referrer, and permissions headers, and a Blazor-aware provider that fails closed on a misconfigured API endpoint.
  • Fail-fast configuration. Settings on the fail-fast contract bind with data-annotation validation and ValidateOnStart(), and two cross-section validators (connection strings, tenancy) catch mismatches a single section cannot see, so a misconfigured host dies at startup, not at first request (ADR-070).
  • Ops as executable practice. SLO alert-to-runbook pairing is a fitness test that reads the Bicep alerts and the operations runbook in both directions; deploy proof-of-recency preconditions, automatic revision rollback, cost baseline as a deploy gate, container image posture (ADR-093), and a tested restore drill with a measured RTO baseline are ADR-documented practices the production apps run.
  • A reference Azure deployment ships with the framework. samples/deployment carries a foundation template (Log Analytics, container registry) and a per-release template (Container App pulling by managed identity, SQL, Key Vault, App Insights, cost tags, a budget with alerts), keeps the SQL connection string as a Key Vault secret read through the app's own identity rather than a plaintext Container App secret, documents federated OIDC for CD with no long-lived cloud credential, and is compiled by a CI job.

Testing and quality gates

The framework ships its own enforcement: adopters subclass, and drift fails the build.

Architecture fitness functions

  • 124 test methods across 47 abstract bases. Thirty-seven are parameterized by your IArchitectureMap and are a three-line subclass per repo; the other ten take an allow-list, a resource assembly, a package-version file, a module type, a source tree, infrastructure files, or a frozen snapshot, with non-vacuity floors so a renamed folder cannot pass with zero findings. The rule bodies live in a 30-file ArchitectureRules library you can also call directly, and CallGraphIndex walks IL so rules catch what signatures cannot: a domain event handler reaching SaveChanges, a hard delete outside the allow-list, raw IQueryable in Application. The families:
    • Layer pairs and layer-map completeness, per-module layer overrides, module isolation across the full internal cross product, module Shared purity, namespace acyclicity
    • Domain and Shared framework purity, Application free of EF Core and ASP.NET Core, transport containment, service contract purity and non-public implementations
    • Entity and aggregate conventions, factory-returns-Result, immutability of DTOs, commands, queries, events, and value objects, naming and sealing across ten type families, domain throw discipline
    • Handler placement, handler-injection bans, constructor arity ceiling, handler result typing, vertical slice cohesion, command validator coverage, cancellation-token conventions
    • Soft delete allow-list and cascade soft delete, specification navigation limits, raw queryable ban, concurrency-attribute placement
    • Idempotency intent on every POST, controller conventions, route authorization, anonymous-endpoint allow-list, PII and anonymizability, data residency versus deployed region (ADR-105)
    • Error-code catalog uniqueness and module prefixes, integration event versioning and base type, upcaster registry rules, frozen wire contracts for events and protos
    • Localization completeness and localized user-visible text, sortable grid columns, brand color tokens, create-form guards, UI code-behind caps, Blazor Server static-state safety
    • Module conformance (declared name, dependencies, disabled stubs), dependency-pin governance, folder width (twelve direct code files per folder), SLO alert-to-runbook pairing
  • Compile-time layer guard for the framework itself. An MSBuild targets file fails a framework build on a forbidden project reference between the seven layered packages before the build resolves, the same rule set the runtime tests assert against compiled assemblies in every consumer.
  • Runtime conformance suites. Eight shipped bases prove you wired the framework correctly: decorator pipeline order, middleware pipeline order, security headers, ProblemDetails contract, OpenAPI contract, service-info versioning, gateway hardening (eight assertions covering throttling, per-route policies, bypasses, correlation, downstream readiness, active probes, and partitioning by forwarded client IP), and graceful shutdown.
  • Guard rails on dependencies. DependencyVersionTestsBase fails if MassTransit reaches v9 or ImageSharp reaches v4 (both commercial-license boundaries); FrameworkVersionConsistencyTestsBase fails if a consumer's framework pins drift out of lockstep.

Integration and unit infrastructure

  • Integration fixtures at three scales. SqlServerIntegrationTestFixtureBase creates one throwaway database and resets it per test; CrossServiceFixtureBase boots several hosts in one process against Testcontainers SQL Server and RabbitMQ with one physical database per logical source, so database-per-module is exercised rather than simulated; ServiceBusEmulatorFixtureBase runs the broker-parity tier against the Service Bus emulator with wall-clock-bounded start and stop phases that name the phase that hung.
  • Host and helper doubles. ProductionHostApplicationFactory boots a host with the environment pinned to Production so restrictive CORS, HSTS, and production-only branches are exercised; JwtTokenGenerator mints tokens and configures in-process validation so a fixture needs no JWKS endpoint; ConfigureTestFeatureFlags layers flags without blanking connection strings; NeutralizeGlobalRateLimiter removes only the global limiter; RecordingHttpForwarder stands in for YARP's forwarder.
  • Unit test ergonomics. HandlerTestBase with an auto-mocked unit of work and repository registration helpers, EntityBuilderBase for arranging aggregates, TestPolling.PollUntilAsync for eventually-consistent assertions without sleeps, and a registration-idempotence assertion for Add* extensions.

Component, E2E, and gates

  • Component test infrastructure. A bUnit base with typed handles to the rendered snackbar, dialog, and popover providers, a mutable authentication state that satisfies both AuthorizeView and direct injection, a list-page host configuration, a request-capturing HTTP double with canned responses, a one-line UI service harness for typed clients, RFC 9457 response factories, error-summary and text-based interaction helpers, and caller-relative markup snapshots (ADR-103).
  • E2E infrastructure. A shared Playwright browser fixture selected by environment (base URL, headless, browser, slow-mo, tracing, timeouts, credentials all configurable), Blazor-render-aware helpers (WaitForBlazorAsync, GotoProtectedAsync, WaitForGridToSettleAsync, verifying FillAndVerifyAsync and ClickAndVerifyAsync), five accessibility-first page objects, and eight inheritable workflow suites (login, registration, logout, password reset, authorization, profile management, user preferences, pseudo-localization). A silent sign-in failure throws instead of passing, and SignOutAsync absorbs the superseded-navigation abort Firefox raises when the app's own redirect overtakes the logout, so the same suites pass on all three engines.
  • Accessibility and performance gates. Two documented axe profiles (strict WCAG 2.1 AA, and the same with one upstream MudBlazor pager defect waived for grid pages) and a Web Vitals collector that samples LCP, CLS, and INP into labelled artifacts and asserts them against a budget.
  • Test platform. Every project runs on Microsoft Testing Platform with xUnit v3; the framework's own CI refuses a run that discovers fewer than 2,000 tests, so a discovery regression cannot pass quietly.

Compliance and privacy

  • PII handling. [Pii] marks personal data; the audit trail redacts marked values at capture so it never becomes a second copy erasure has to chase; PiiRedactor (Redact, RedactToString, HasPii) is the opt-in helper for log-safe rendering; a convention test keeps PII markings honest.
  • GDPR data export (DSAR). ExportUserDataHandlerBase composes per-module IUserDataExportSection contributions registered with AddUserDataExportSection into a versioned envelope where each section reports availability and a caller-safe reason, so an unreachable peer costs one section rather than the document. DataExportControllerBase is authorized and feature-gated at the class level, off until PrivacyFeatures.DataExport is enabled, and returns a dated JSON file download.
  • Erasure and anonymization. DeleteUserHandlerBase runs owner-or-role authorization, soft delete, anonymize-in-place, save, the soft-deleted-user cache marker (best effort, never a failure the caller would retry against an already-erased account), and then a post-commit tail through IErasableUser; IAnonymizable distinguishes true erasure from soft delete.
  • Encryption at rest. Field-level AES-256-GCM column encryption with a versioned key ring and zero-downtime rotation (section 5).
  • Retention across three tables. Outbox payloads, refresh-session device records, and audit trail rows each have their own configured window and sweep; refresh sessions are hard-deleted because the row is a credential digest.
  • Data residency. A fitness test base asserts the policy text against the deployed region your fixture extracts from infrastructure (ADR-105).
  • Audit evidence. The field-level audit trail provides the who-changed-what record, stamped with trace id and tenant so a compliance question and a telemetry trace answer each other.
  • Signed-in pages stay out of shared caches. Authenticated HTML responses carry Cache-Control: no-store, so a signed-out user pressing back never sees the previous render.

Governance, versioning, and supply chain

  • 111 ADRs. Every framework pattern is documented as an architecture decision record with context and consequences, published publicly and indexed in one place; adopters inherit the reasoning, not just the code.
  • Lockstep SemVer with a stated policy. All 17 packages release at one version from a git tag; consumers bump once per release, no phased rollout (ADR-016). Breaking changes ship as minor bumps inside 1.x with the call site named in the CHANGELOG, and there is no obsolete grace period, so read the entry at bump time.
  • An upgrade map for every breaking release. UPGRADING.md at the repo root carries one section per breaking release, newest first, with the old-to-new map and the mechanical fix (a namespace move is a using rewrite; a constructor change names the parameter to forward), kept identical to the CHANGELOG block so a consumer outside the workspace has the same recipe the in-workspace script applies.
  • The public API surface is a reviewable diff. Every packable project declares its shipped surface in PublicAPI.Shipped.txt, and the build fails on an undeclared public member or a disappeared declared one, so widening or breaking a package API is caught in review rather than by a consumer after release.
  • Supply-chain provenance. CycloneDX SBOM on both release jobs, committed lock files, a build-time vulnerability audit promoted to error, and package source mapping cleared to a single nuget.org source as a dependency-confusion defense. The nuget.org push is keyless OIDC trusted publishing pinned to the release workflow file; GitHub Packages receives the same set via the workflow token (ADR-053).
  • Eight required merge checks, and more that run. Build and test with a FACTS drift gate, the Windows MAUI build across four targets, three browser accessibility legs, coverage (68.3 percent floor on the unit tier), the Helpdesk consumer canary (source build plus real migrations against ephemeral SQL Server), and the performance gate are required; a package-mode consumer build, Redis behavior against a real server, and Bicep compilation also run on every PR.
  • Generated, gated facts. FACTS.md is produced from source (version, package list, fitness counts) and CI fails when the committed file drifts, so counts cannot rot in prose.
  • Dependency updates arrive grouped and policy-aware. Weekly Dependabot with analyzer, OpenTelemetry, and test-stack groups, plus commercial-license ceilings mirrored as ignores beside the fitness function that enforces them; ADR-016 records the MassTransit v8 support horizon and the exit candidates.
  • Security invariants that are tests, not prose. Private advisory reporting, an allow-listed scan of every anonymous endpoint across controllers and routable pages, known-answer PBKDF2 tests pinning iteration count and salt size, and unit tests proving permissive CORS is never combined with credentials.
  • A living reference app. MMCA.Helpdesk exercises one module through all five layers, doubles as the template source, and pairs step by step with the getting-started guide; a two-module e-commerce sample shows the multi-module shape.
  • Production proof. Two full applications (MMCA.Store e-commerce with Stripe, MMCA.ADC conference platform with native mobile apps) run on the framework in Azure production, so every capability above is exercised by real deployed software.
  • Shared analyzer baseline. Five analyzers (Meziantou, Sonar, StyleCop, Roslynator, VS Threading) at error severity plus the public API analyzer, under TreatWarningsAsErrors, with the baseline kept in sync across framework and consumers by a comparison script.
  • Governance you can copy. A versioned 34-category evaluation rubric (maturity 0 to 4 and implementation 0 to 10 per category; version 2 realigned two categories in place into Messaging and Integration Architecture and AI-Native Application Architecture, ADR-110) with an evidence-cited scorecard and remediation backlog per repo, CODEOWNERS with a documented review ratchet, PR and issue templates, a code of conduct, and a scoped-commit convention tying commits to rubric categories and ADR numbers. Apache-2.0 on every package with an explicit patent grant; the CHANGELOG, icon, and source-linked symbols ship inside each nupkg.

Documentation library

This library holds ten framework guides, the ADR index, the governance rubric with scorecards, and a chapter-per-subsystem onboarding walkthrough.

  • Getting Started. Nothing to a running, migrated app in six steps from the scaffold, with MMCA.Helpdesk as the worked companion.
  • Templates. Every parameter of the four templates and of the add-module script.
  • Building by Hand. The same solution phase by phase, for retrofitting an existing codebase.
  • Build MMCA.ECommerce. A two-module Products plus Orders sample built end to end.
  • Small Apps. The --database sqlite --no-aspire floor and when to flip each switch back on.
  • Versioning and Breaking-Change Policy. SemVer, lockstep, what counts as breaking, and the absence of an obsolete grace period.
  • Accessibility. The WCAG 2.1 AA target and the split between the axe gate and the manual screen-reader pass.
  • Resilience and Business Continuity. What a library can guarantee versus what the consumer's infrastructure owns, plus the restore-drill baseline.
  • Responsive Design and Cross-Browser Support. The supported viewport, touch-target, and browser-engine matrix.
  • Cost and FinOps Notes. The cost-relevant defaults the framework ships and the levers consumers should set.
  • ADRs, governance, onboarding. 111 decision records with one-line summaries; the 34-category rubric with three scorecards and three backlogs; thirty-plus onboarding chapters walking every first-party type plus DevOps chapters for Aspire, CI/CD, infrastructure, runbooks, and testing. In the repo: FACTS.md, CHANGELOG.md, UPGRADING.md, SECURITY.md, and the deployment sample's DEPLOYMENT.md.

Constraints worth knowing

Design choices an adopter should read before sizing a plan around them. None is a defect; each is documented in source or an ADR.

  • The outbox is off in in-process mode. With MessageBus:Provider=InProcess nothing is written to the outbox and the processor is not registered; the durable path switches on when a broker is configured (ADR-100).
  • Encrypted columns are not queryable. Ciphertext is non-deterministic, so an encrypted column supports no equality or range predicate, unique index, or server-side sort; a filter on it silently returns no rows.
  • Edge rate limits count per replica. The gateway limiters live in process memory, so the effective allowance is the configured number times the replica count; a fleet-wide limit belongs on the service-side distributed limiter.
  • Breaking changes ship as minor versions. Inside 1.x the version number is not a breakage signal and there is no [Obsolete] window; the CHANGELOG entry names each call site to touch.
  • A folder move is a public API rename. Namespaces follow folders across a lockstep-released package family, so reorganizing the framework's own layout ships as a breaking release with an UPGRADING.md map; the fix is a mechanical using rewrite with no configuration, database, or behavior change.
  • The soft-deleted-user gate fails open. A confirmed deleted account gets 401; a cache or validator error is logged and the request proceeds.
  • Renaming an event with pending rows is two steps. [EventName] affects rows written after it is applied; drain the outbox, then rename.
  • The audit trail reader reads one database. In a database-per-module host it returns the trail of the modules in the configured source; retention needs the scheduler enabled.
  • The single-database integration fixture is single-database. Multi-source reset is the cross-service fixture's job, and hosts with several physical sources must reset every one.
  • Not every settings class validates on start. Those on the fail-fast contract do; the security-headers section, for example, binds without validation.
  • Source mode can mask package-mode failures. A green local.props build does not prove a package-mode Release build is green; the package-consumption CI job exists for that reason.
  • The MAUI package builds separately. MMCA.Common.UI.Maui lives outside the main solution and is built and packed by Windows jobs (ADR-042).

The one-paragraph pitch: adopt MMCA.Common and a small team gets, on day one, the architecture a platform team would take years to build: a scaffolded modular monolith with errors-as-values, a sealed CQRS pipeline, reliable messaging, multi-tenancy, full identity, a shared Blazor, desktop, and mobile UI layer, and 124 architecture tests guarding it all, plus a proven, host-only path to split into microservices when (and only when) an observable constraint demands it.


Compiled 2026-09-06 against MMCA.Common v1.187.0. The authoritative package and fitness counts live in FACTS.md and the ADR range in the ADR index; the figures above are a snapshot at that version. Every capability is verified in framework source and in consumer use (MMCA.ADC, MMCA.Store, MMCA.Helpdesk).