Architecture Decision Record
ADR-101: An MMCA.Common Metapackage for the Core Six
Status
Accepted (2026-08-29, framework v1.170.0). Extends ADR-053 (the metapackage ships to both registries from the same tag, through the same workflow) and inherits ADR-016 unchanged (it releases at the same version as everything else, and adds one more entry to the set a consumer sweeps together). The authoritative package list and count live in FACTS.md; this record does not restate them. Revised 2026-10-07: the lockstep rationale now says the metapackage sets its six dependencies as minimum-version floors rather than exact pins, and the host claim says application hosts depend on all six mostly transitively; anchors refreshed after the v1.233.0 release.
Context
ADR-053 made the install path credential-free: dotnet add package MMCA.Common.API works for anyone,
from nuget.org, with no token and no hand-written nuget.config. What it did not shorten is the
number of lines that follow. A standard application pins six packages before it pins anything of
its own (its application hosts depend on all six, mostly transitively), and their names are the
layer names, so the list reads as the architecture but also as six chances to get an incomplete
set: an app that takes Application and Infrastructure but not API compiles until the first controller, and one that omits Aspire compiles until
AddServiceDefaults.
The friction lands hardest exactly where this release is trying to lower the floor. A reader
following the getting-started path, or an adopter retrofitting an existing solution by hand, types
six PackageReference elements plus six PackageVersion pins under Central Package Management,
twelve lines of ceremony before a using directive.
Nothing about the layer packages themselves is wrong: the split is what makes the layer rules
enforceable at the package level and lets a Blazor WebAssembly project take MMCA.Common.Shared
alone. The question this record answers is only whether the common case has to be spelled out every
time.
Decision
Publish a metapackage named MMCA.Common that carries dependencies and no code.
It bundles the Core 6, in layer order.
Source/MMCA.Common/MMCA.Common.csproj:26-31references Shared, Domain, Application, Infrastructure, API and Aspire, ordered so the bundle reads as the architecture it installs (:23-25). OnePackageReferencereplaces six.It ships no assembly.
IncludeBuildOutputisfalse(:9) because a metapackage ships dependencies, not code: the project has no compile items of its own, and packing its empty assembly would add a DLL every consumer loads for nothing (:5-8).NU5128(a dependency group with no matchinglib/folder) is suppressed in this project alone, because that warning describes exactly the shapeIncludeBuildOutput=falseis meant to produce andTreatWarningsAsErrorswould otherwise turn it into a pack failure (:10-14).It is an ordinary packable project in every other respect. Version comes from MinVer off the same tag, packaging metadata from
Directory.Build.props, and the repository README is packed with it (:34). It therefore publishes to both registries in the same release run with no workflow change (ADR-053).UI, Gateway, Grpc, Aspire.Hosting and the Testing packages stay out. The bundle is what a standard application host needs. A Blazor host takes
MMCA.Common.UI/UI.Web, a gateway takesMMCA.Common.Gateway, a service exposing gRPC takesMMCA.Common.Grpc, an AppHost takesMMCA.Common.Aspire.Hosting, and test projects take theTesting.*set. Each of those belongs to a specific project in the solution rather than to every host, andMMCA.Common.UI.Mauicould not be included in any case: it is the one MAUI-TFM package, built outside the solution by dedicated Windows jobs (ADR-042), so anet10.0metapackage cannot reference it.The name is the bare prefix, deliberately.
MMCA.Commonwas the one unused id in the family and it is the one a reader guesses first. It carries the ADR-016 lockstep contract like every otherMMCA.Common.*id.Nothing switches to it. ADC, Store and MMCA.Helpdesk keep their explicit per-layer pins, and the
MMCA.Templatesscaffold still generates the six-line set. The metapackage is an option for a new adopter, not a migration anyone is asked to run.
Rationale
- The common case should cost one line. Six references is not hard, it is repetitive and order-independent noise that a reader has to verify rather than read, and getting it wrong fails later than it should.
- The set is not arbitrary. Every application in this workspace pins all six under Central
Package Management (among its other
MMCA.Common.*entries), and its application hosts (API, service and Blazor web hosts, not the YARP gateway, which takes onlyMMCA.Common.AspireandMMCA.Common.Gateway) end up depending on all six, although none references all of them directly: a host referencesMMCA.Common.Aspire(plusMMCA.Common.APIin the ADC and Store Blazor web hosts) and receives the other layers transitively through its module projects or the API package's own dependencies. Bundling a set that real hosts already depend on together is a shortcut, not a new opinion about what belongs where. - A code-free package cannot drift from the layers. With no assembly, there is nothing for the metapackage to expose, deprecate or version independently: it is a list of dependencies, and the layer rules keep being enforced by the packages it points at.
- Lockstep makes it safe. Because every package ships at one version from one tag (ADR-016), the
metapackage declares its six dependencies at a minimum of its own version (plain
ProjectReferenceitems packed as NuGet floors, not exact[X]ranges), and a consumer bumping it raises all six. The floor does not forbid a consumer from also pinning a layer entry above the metapackage (the layers also take each other as floors, so raising one above a directly pinned layer it depends on is an NU1605 downgrade error instead); ADR-016's one-pass sweep, not the metapackage, keeps the set in agreement. - Excluding the specialised packages keeps the bundle honest. A metapackage that also pulled UI and Testing would put MudBlazor and Playwright into a headless API host, which is the failure mode that makes people distrust metapackages in the first place.
Trade-offs
- A consumer mixing both forms has more pins to keep in step, not fewer. An app taking
MMCA.CommonplusMMCA.Common.UIand theTesting.*set still has to sweep every entry to the same version in one pass (ADR-016), and the metapackage is one more entry in that sweep rather than a replacement for it. Nothing in the bundle relaxes the no-phased-rollout rule. - It hides which layer a type came from. Part of the value of the split is that a
PackageReferencelist states the layers a project participates in. A single reference gives that up, which matters most in the project where it matters least (the host, which depends on all of them anyway). - It can pull more than a project needs. A host that genuinely wants no Aspire dependency should keep the explicit five rather than take the bundle: the metapackage has one shape, and trimming it per consumer would defeat the point.
- NU5128 is suppressed rather than avoided. The suppression is scoped to this project and justified in place, but it is a warning switched off, and a future genuine packaging problem in this project would have to be caught by the pack output rather than by the build.
- No consumer exercises it. The package-consumption CI job packs every package in
MMCA.Common.slnx(MMCA.Common/.github/workflows/ci.yml:671), the metapackage included, so a pack break is caught, but it never restores the metapackage: its throwaway consumer references onlyMMCA.Common.API,MMCA.Common.InfrastructureandMMCA.Common.Testing.Architecture(:706-708), its layer-rule probes take only Domain, Application and Infrastructure (:750-757), and theMMCA.Common.*source-mapping pattern (:690) would not route the bareMMCA.Commonid. No application in this workspace builds against the metapackage today, so its ergonomics are asserted rather than demonstrated. That is a deliberate consequence of point 6.
Revision (2026-10-07)
Re-verified against current source. The decision is unchanged: MMCA.Common still bundles Shared,
Domain, Application, Infrastructure, API and Aspire in layer order
(MMCA.Common/Source/MMCA.Common/MMCA.Common.csproj:26-31), still ships no assembly (:9) with
NU5128 suppressed in this project alone (:14), and no application or CI consumer restores it. Two
rationale statements were stated more strongly than source supports and are corrected in place.
- The six dependencies are floors, not exact pins. The metapackage references the six through
plain
ProjectReferenceitems (MMCA.Common/Source/MMCA.Common/MMCA.Common.csproj:26-31), which NuGet packs as bareversion="X"dependencies, a minimum-inclusive range, and nothing inMMCA.Common/Directory.Build.propsrewrites them to exact[X]ranges. The floor equals the metapackage's own version, so bumping it raises all six, but a consumer can still pin a layer entry above the metapackage without a restore error, unless it also pins, at the old version, a layer the raised one depends on (the layers take each other as floors too, for exampleMMCA.Common/Source/Presentation/MMCA.Common.API/MMCA.Common.API.csproj:33-34, so that case is an NU1605 downgrade). Agreement of the set comes from the ADR-016 one-pass sweep, and the Rationale bullet now says so. - Hosts depend on the six mostly transitively. No host in ADC, Store or MMCA.Helpdesk
references all six directly, and the ADC and Store gateway hosts depend on none of the layers
below Aspire (they take only
MMCA.Common.AspireandMMCA.Common.Gateway,MMCA.ADC/Source/Hosts/MMCA.ADC.Gateway/MMCA.ADC.Gateway.csproj:3-4); the Context, Rationale and Trade-offs now say application hosts depend on all six. The Helpdesk web host references onlyMMCA.Common.Aspire(MMCA.Helpdesk/Source/Hosts/MMCA.Helpdesk.Web/MMCA.Helpdesk.Web.csproj:10) and gets the other layers through its module and migrationsProjectReferenceitems (:13-14); the ADC and Store Blazor web hosts reference Aspire and API (MMCA.ADC/Source/Hosts/UI/MMCA.ADC.UI.Web/MMCA.ADC.UI.Web.csproj:14-15,MMCA.Store/Source/Hosts/UI/MMCA.Store.UI.Web/MMCA.Store.UI.Web.csproj:16-17). What every application does take as a set is the sixPackageVersionpins (MMCA.Helpdesk/Directory.Packages.props:84-89). The Rationale bullet now states that. - Anchors re-verified against current source: the pack step is
MMCA.Common/.github/workflows/ci.yml:671(it packs everyMMCA.Common.slnxpackage, the metapackage included), the throwaway consumer's threePackageReferenceitems are:706-708, the layer-rule probes that also never take the bare id are:750-757, and theMMCA.Common.*source-mapping pattern is:690.
Related
ADR-053 (dual-registry publishing, which the metapackage
joins with no workflow change),
ADR-016 (one version for every package, one sweep per
consumer: the property that makes bundling safe),
ADR-042 (MMCA.Common.UI.Maui, the MAUI-TFM package built
outside the solution and therefore unreferenceable from a net10.0 bundle),
ADR-015 (the layer rules and the public-API surface gate that
keep operating on the individual packages the bundle points at),
ADR-065 (the scaffold, which is the other answer to the same
adoption-floor problem and still emits the explicit set).