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.Templatesthendotnet new mmca-appgenerates 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-commandandmmca-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-upsdotnet newcannot 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-versionto 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
TreatWarningsAsErrorswith 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.Commonmetapackage bundles the core six packages as one reference. - A deliberately low floor.
--database sqlite --no-aspiregives 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.propsflips everyPackageReferenceto aProjectReferenceagainst 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-smokeCI 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.Templatesships ontemplates-vX.Y.Ztags 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/Resultwith nineErrorTypeclassifications (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, andTask>mirrors of each. Failures map to RFC 9457 ProblemDetails at the HTTP edge and to gRPC status on the wire, andProblemDetailsResultReaderreads them back into typed errors on the client, so pages branch instead of catch. - Results survive a cache.
ResultJsonConverterFactorymakesResultround-trippable through Redis, so a cached query result keeps its typed failure shape. - A reusable invariant library.
CommonInvariantsships 22Result-returning guards (string bounds, ids, enums, ranges, URLs, time zones, uniqueness, collection size, culture, theme) that module invariant classes compose withResult.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.
AddMmcaApplicationPipelineseals the composition: any framework registration helper called afterwards throws, andVerifyDecoratorPipeline()exposes the check to fitness tests for anything registered by hand. An opt-in profiling decorator pair is available viaAddApplicationProfiling(). - 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/IQueryon a request letCqrsContractInspector.FindContractMismatchesfail a build when a handler's return type or kind drifts from what the request declared. - Convention-based registration.
ScanModuleApplicationServicesauto-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. TheMmcaApplicationPipelineBuildercallback offersScanModule,ScanModules, andRegisterfor cross-service client wiring. - Guard-clause extensions.
ICurrentUserService.RequireUserId(...)collapses the read-then-null-check block into oneResult, andIReadRepository.GetByIdOrFailAsyncreturns aNotFoundfailure 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:SetItemswith a validation hook,GetChildOrNotFound,RemoveChildOrNotFound,RestoreChild, and cascadingDeleteChildren). Factory methods returnResult. - Value object library. Seven
ValueObjecttypes:Money,Currency,Email,PhoneNumber,Address,DateRange,DateTimeRange, each with aResult-returning factory; Address, Email, and PhoneNumber carry separate invariant classes. EF value converters ship for Email, PhoneNumber, andEnumeration;Moneymaps as an owned type; Currency and Enumeration have JSON converters. - Smart enums are opt-in. Plain enums by default,
Enumerationwith 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/QuerySpecificationwith criteria, ordering, includes, paging, tracking control, and.And()/.Or()/.Not()composition.CrossSourceSpecification.BuildAsyncresolves a principal's keys in its own database and returns a portablekey 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 plusRegister);ModuleLoaderdiscovers them by reflection and registers them in topological (Kahn) order. Seeding is a separateIModuleSeedercontract invoked in the same order. A module disabled byModulesSettingsgets stub registrations so cross-module interfaces still resolve, andRemoteDependenciesdeclares that a disabled dependency is satisfied out of process. - Identifier type aliases. Entity ID types are solution-wide
global usingaliases 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/), andFolderWidthTestsBasemakes 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 (
AddModuleHostboots one module per process), a.Contractsproto 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.
AddTypedGrpcClientgives service discovery over h2c, JWT forwarding, the standard resilience pipeline, and a pinnedSocketsHttpHandlerso HTTP/2 negotiation and connection rollover survive the global client defaults.AddGrpcServiceDefaults()adds theResultinterceptor, turns detailed errors off, and enables reflection.ResultGrpcExtensionsmaps all nine error types in both directions, so a caller branches onResultinstead of catchingRpcException. Any*.Contractsproject 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
IEventBuspublish call works in-process (monolith) or over RabbitMQ / Azure Service Bus (extracted), selected purely byMessageBus: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.
MicroserviceExtractionTestsBasefails the build if MassTransit, Grpc, or Protobuf types reach Domain, Application, or Shared;ServiceContractPurityTestsBasekeeps a[ServiceContract]free of the producer's internals;ContractImplementationTestsBasekeeps implementations non-public;ProtoContractTestsBasediffs 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, orDefault. One sealed context class per engine (SQLServerDbContext,CosmosDbContext,SqliteDbContext) over the abstractApplicationDbContext, 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.
INavigationPopulatorbatch-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/IUnitOfWorkwith transaction control, narrowedIEntityReader/IEntityQuerierfor read paths, anExecuteUpdatebuilder for set-based writes, and a repository factory that wraps every repository in MiniProfiler timing when profiling is on. - Schema lifecycle on one interface.
IDbContextFactoryexposesMigrateAsync,HasPendingMigrationsAsync,EnsureCreatedAsync, andRequestIdentityInsertacross every physical source;InitializeDatabaseAsyncdrives per-source migration and module seeding from aDatabaseInitStrategy.
Cross-cutting behavior
- Soft delete done right.
IsDeletedflag, a namedSoftDeletequery filter that can be dropped independently of theTenantfilter, filtered unique indexes so a deleted row does not block re-creating the same value (ADR-095),HasSoftDeleteFilter()for hand-authored indexes,RestoreChildfor reversible deletes, and cascade-to-children enforced by a fitness rule. - Automatic audit stamping and concurrency.
CreatedOn/By,LastModifiedOn/By,DeletedOn/Bystamped by a save interceptor, andRowVersionconfigured as the concurrency token on every auditable entity by the base context, so no entity configuration opts in. - Optimistic concurrency via ETag / If-Match.
RowVersionround-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, exposesIAuditTrailReader, and sweeps on a retention window through the scheduler. - Field-level encryption at rest.
EncryptedStringConverterencrypts 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.
ExecuteInTransactionAsyncis 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 failedResultexactly as on a throw, and never retries the commit: a commit whose outcome is unknowable escapes asTransactionCommitAmbiguousExceptionnaming each physical source as committed, ambiguous, or rolled back (ADR-107).IUniqueConstraintViolationDetectorturns a lost insert race into a typed conflict instead of a 500.
Jobs, paging, migrations
- Persistent cron scheduler.
IScheduledJobwith 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.
PeriodicBackgroundServicegives an enablement gate, startup delay, a fixed-interval loop that survives a failing cycle, andTimeProvider-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-Paginationmetadata, a 1,000-row ceiling on any unbounded read inside the query pipeline, andIdappended 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.
IDbSeederhandles int or Guid seed identifiers, andIdentityModuleDbSeederBaseseeds 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:CommandTimeoutSecondsknob applies to every relational source;CosmosIntIdValueGeneratorcovers 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
OutboxMessagesin 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 forOutboxProcessor. 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 implementingIHasOrderingKey, 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.
IOutboxAdministrationlists 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.
EfInboxStorededups 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 byMessageBus: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.EndpointPrefixnamespaces queues on a shared broker; a broker paired withEnableOutbox=falsefails at startup; a host running with the outbox or inbox off logs one startup notice. - Schema-versioned events with upcasting.
BaseIntegrationEventcarries aSchemaVersion;IEventUpcasterregistrations migrate old payloads on consumption.EventUpcasterRegistryrejects 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.
AddDomainEventon 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),RegisterUpcastedIntegrationEventConsumerfor draining a retired contract, andRegisterOutputCacheEvictionConsumer. - Handler bases that keep the retry promise honest.
SafeDomainEventHandlerlogs a failure with full context and then lets it propagate so the outbox actually redelivers;ScopedIntegrationEventHandlerBasegives 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.ExecuteAsyncstandardizes 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.
EntityChangedEventwithDomainEntityState, andOutputCacheEvictionRequestedas 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.
EntityControllerBasegives 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 viaRegisterStrategy), 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.
GetReadSpecificationAsyncis 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.
GetByIdAsyncpublishes the DTO'sRowVersionas a weak ETag; a protectedSetConcurrencyETaglets hand-written reads emit the identical header. - CSV export with a stated ceiling. Every export sends
X-Export-Row-Limitup 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.
AggregateRootEntityControllerBaseships idempotentPOSTandDELETE;CrudEntityControllerBaseaddsPUTcarrying both[Idempotent]and[SupportsIfMatch].AddEntityCrud()registers the create, update, and delete commands and the update validator bridge;AddEntityUpdateVerbandAddEntityUpdatecover derived update commands that carry route-derived ids or server-decided flags (ADR-099). - Handler bases for richer writes.
MutateEntityHandlerCoresupplies the load-mutate-save workflow with three subclasses for the three return shapes, plusCreateEntityHandlerBaseand the add-child / remove-child pair for aggregate children.MutationContextcarries typed values from load to post-save hooks, andSkipSave()is an idempotent no-op signal. - Validators written once, against the request.
CommandRequestValidatorbridges 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 ajavascript:/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 byIdempotency-Keyfor 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 withX-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 youMiddlewarePipelineBuilderto insert, replace, or remove steps by name, andBuild()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
requestIdextension on every ProblemDetails;UnhandledResultFailureFilterrewrites a failingResultthat an action returned insideOk(...)into the correct ProblemDetails status instead of a 200. - Service-side rate limiting.
AddCommonRateLimitingbinds an eight-knobRateLimitingsection: permit and queue limits, per-user and global limits, fixed or sliding window, andDistributed, which moves counters onto Redis and degrades to per-instance when no multiplexer exists. A dedicatedauth-ippolicy 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-infoendpoint, 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
IFeatureGatedshort-circuiting in the CQRS pipeline. - Localized error codes and culture.
AddErrorResources()per module returns localized, coded errors the UI displays directly;UseCommonRequestLocalization()andMapCultureEndpoint()(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;
AddModuleHealthChecksemits onemodule-{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;MapAppAssociationEndpointsserves Apple app-site-association and Android asset-links for deep links;AddMiniProfilerIfEnabledmounts/profilerbehind 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.
TokenServicesigns with RS256 by default (HS256 selectable by configuration for a single host);MapJwksEndpointandMapOidcDiscoveryEndpointserve/.well-known/jwks.jsonand/.well-known/openid-configuration; every service validates throughAddForwardedJwtBearerwith 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.
RequireHttpsMetadataresolves 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_tokenfor/hubspaths, because WebSocket connections cannot send anAuthorizationheader. - 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;
TryRotateAsyncmakes 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, andRevokeAllSessionsAsyncbehind an additivesidclaim; 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.
ILoginProtectionServiceapplies 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.
IPasswordResetTokenServicekeeps 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 itsClientId, with a ten-minuteExternalLogincookie 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 aPOST exchangeaction 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 withAddPermissions, and a host that never declares any gets a registry that denies everything and logs once naming the fix.RoleValuegives case-insensitive role equality against a per-app known set. - Resource-ownership authorization.
OwnerOrAdminFilterreads 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.CheckOwnershipwithIUserOwnedRequest/IUserScopedRequestmarkers 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.
AuthenticationServiceBaseimplements 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 implementICacheInvalidating, 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 byCache:PopulateLockTimeout; failed results are never cached; hits and misses are metered. - Swappable cache substrate. Memory, distributed Redis, or opt-in HybridCache behind one
ICacheService, withCache:KeyPrefixnamespacing every key on a shared Redis, a 30-second default TTL, an L1 ceiling for the two-level cache, a disjointhc:keyspace so the two implementations never meet at one key, andRemoveByPrefixAsyncand atomicIncrementAsyncbeyond get and set. Every call is fail-open: a cache outage degrades to uncached reads. - Distributed locking ships with the cache.
IDistributedLockis 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 sameAddCachingcall. 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()andAddRedisOutputCaching()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-onlyredischeck 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).EvictTagsAsyncand best-effortTryEvictTagsAsyncevict multiple tags at once; cross-host eviction is one DI call plus one bus registration, driven by theOutputCacheEvictionRequestedevent. - Warm-up before ready.
IWarmupTaskregistrations run before/health/readyreports 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, andSelfHttpWarmupTaskBasereplays 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.
IUiReadCacheis 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 theEntityServiceBaseconstructor. - Typed clients retry with a constant idempotency key.
AuthenticatedServiceBaseretries transport failures, 5xx, 408, and 429 three times with 2s, 4s, 8s backoff plus jitter, disposing each retried response, andHttpResultExecutorturns transport failures and timeouts into typedResulterrors. - Offline and stale-load guards.
OfflineFirstPageSnapshotserves a list page's first page from device storage when offline;LatestLoadGuardcancels 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, andDisableHttpClientMetrics/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 itsIUIModuledescriptor; 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.
DataGridListPageBasewires 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.
EntityServiceBasetyped clients returnResultfor every outcome, andResultUiExtensionsadds localized error lists,NotifyOnFailurestraight to a toast, andIsNotFound/IsUnauthorizedpredicates so a page branches on classification instead of catching. - Vendor-neutral toast and dialog facades.
IToastServiceandIAppDialogServiceare 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,
ReturnUrlProtectoragainst open redirects, a drop-inBiometricGate, and a native OAuth broker for the system-browser round trip. - Client-side validation parity.
PasswordComplexityAttributeandAbsoluteUrlAttributemirror the server rules so anEditFormgives 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/LeaveChannelAsyncreference-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,IPublicLinkBuilderso shared links never point at the WebView's internal origin, andIFormFactorper 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, andAddMauiPushDeviceTokenProvider()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),AddMessageBrokerandAddServiceBusEmulatorBrokerwith matchingWithBrokeroverloads,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, andoptional, the last so a degradable dependency shows on/healthwithout 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.
ConfigureEndpointsWithHealthProbeconfigures cleartext HTTP/2 (for gRPC behind a gateway) or mixed HTTP/1+2 profiles, and adds a dedicated HTTP/1.1 listener whenHealthProbe:Portis set, because platform probes speak HTTP/1.1 and an HTTP/2-only endpoint answersHTTP_1_1_REQUIRED.WithH2cHealthCheckgates AspireWaitForedges on a prior-knowledge probe and defaults to/alive, since gating startup on readiness can deadlock the graph. - Module hosting.
AddModuleHostbinds 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.
AddGatewayDownstreamHealthChecksregisters 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-knownalways 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-specificBypassPathPrefixes, 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()stampsX-Correlation-ID(preferring the W3C trace id) so the service-side middleware adopts the same id, with no dependency beyond the request delegate;AddCommonGatewayCorsgives 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.
AddCommonSerilogregisters 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.
AddCommonKeyVaultConfigurationlayers a vault over host configuration under managed identity, gated onKeyVault:Uriso local hosts take no Azure dependency, with a reload interval so a rotated secret reaches the process without a redeploy.AddCommonDataProtectionpersists 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.
AddAzureBlobFileStoragebehindIFileStorage, andSmtpEmailSenderbehindIEmailSender, 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 throughCspNonce.Getas the supported way offunsafe-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/deploymentcarries 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
IArchitectureMapand 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-fileArchitectureRuleslibrary you can also call directly, andCallGraphIndexwalks IL so rules catch what signatures cannot: a domain event handler reachingSaveChanges, a hard delete outside the allow-list, rawIQueryablein 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.
DependencyVersionTestsBasefails if MassTransit reaches v9 or ImageSharp reaches v4 (both commercial-license boundaries);FrameworkVersionConsistencyTestsBasefails if a consumer's framework pins drift out of lockstep.
Integration and unit infrastructure
- Integration fixtures at three scales.
SqlServerIntegrationTestFixtureBasecreates one throwaway database and resets it per test;CrossServiceFixtureBaseboots 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;ServiceBusEmulatorFixtureBaseruns 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.
ProductionHostApplicationFactoryboots a host with the environment pinned to Production so restrictive CORS, HSTS, and production-only branches are exercised;JwtTokenGeneratormints tokens and configures in-process validation so a fixture needs no JWKS endpoint;ConfigureTestFeatureFlagslayers flags without blanking connection strings;NeutralizeGlobalRateLimiterremoves only the global limiter;RecordingHttpForwarderstands in for YARP's forwarder. - Unit test ergonomics.
HandlerTestBasewith an auto-mocked unit of work and repository registration helpers,EntityBuilderBasefor arranging aggregates,TestPolling.PollUntilAsyncfor eventually-consistent assertions without sleeps, and a registration-idempotence assertion forAdd*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
AuthorizeViewand 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, verifyingFillAndVerifyAsyncandClickAndVerifyAsync), 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, andSignOutAsyncabsorbs 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).
ExportUserDataHandlerBasecomposes per-moduleIUserDataExportSectioncontributions registered withAddUserDataExportSectioninto a versioned envelope where each section reports availability and a caller-safe reason, so an unreachable peer costs one section rather than the document.DataExportControllerBaseis authorized and feature-gated at the class level, off untilPrivacyFeatures.DataExportis enabled, and returns a dated JSON file download. - Erasure and anonymization.
DeleteUserHandlerBaseruns 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 throughIErasableUser;IAnonymizabledistinguishes 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.mdat 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 ausingrewrite; 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.mdis 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-aspirefloor 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'sDEPLOYMENT.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=InProcessnothing 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.mdmap; the fix is a mechanicalusingrewrite 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.propsbuild 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.Mauilives 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).