Skip to content

Storage-type alias packages are not composable: two in one dependency graph produce 220 duplicate global usings #244

Description

@matt-edmondson

Found while building an application that wants all four storage types at once.

What happens

Each of ktsu.Semantics.Quantities.{Double,Float,Decimal,Precise} ships its alias props to buildTransitive/:

<None Include="buildTransitive\ktsu.Semantics.Quantities.Precise.props" Pack="true" PackagePath="buildTransitive\" />

buildTransitive is, by design, the folder whose contents flow to transitive consumers — not just to the project that references the package. All four packages then emit the same 220 alias names:

<Using Include="ktsu.Semantics.Quantities.Mass&lt;double&gt;" Alias="Mass" />
<Using Include="ktsu.Semantics.Quantities.Mass&lt;ktsu.PreciseNumber.PreciseNumber&gt;" Alias="Mass" />

So any project with two of them anywhere in its graph gets two global using Mass = …; in its generated GlobalUsings.g.cs, and the build dies with 220 × (n−1) errors:

obj/Debug/net10.0/Foo.GlobalUsings.g.cs(10,14): error CS1537: The using alias 'AbsorbedDose' appeared previously in this namespace
obj/Debug/net10.0/Foo.GlobalUsings.g.cs(11,14): error CS1537: The using alias 'AbsorbedDose' appeared previously in this namespace
… 658 more

Reproduction

Foo.Storage.Double/   → PackageReference ktsu.Semantics.Quantities.Double
Foo.Storage.Float/    → PackageReference ktsu.Semantics.Quantities.Float
Foo.App/              → ProjectReference to both

dotnet build Foo.App fails. Four facades give 660 errors.

Why this is worse than it sounds

The documented rule — "Use one storage-type alias package per project" — is satisfied by every project here. Each facade references exactly one. The rule is about the project that declares the reference, but buildTransitive makes the binding apply to everything downstream, and a downstream project cannot satisfy a rule about a package it never referenced.

The diagnostics make it hard to find:

  • Nothing in the error names an alias package, or any package. It points at a generated file the author did not write.
  • The failing project may reference no alias package at all — the collision arrives entirely through project references.
  • 220 errors per extra package buries anything else in the build.
  • It scales with the number of storage types, so the more of the library's own design you use, the worse it gets.

This is the natural shape for an application that compares storage types, which is exactly what the four packages exist to enable.

Workaround

PrivateAssets="all" on every alias PackageReference:

<PackageReference Include="ktsu.Semantics.Quantities.Precise" PrivateAssets="all" />

Correct on the merits — the binding is an implementation detail of the wrapping project rather than part of its contract — but it has to be known in advance, and nothing points you to it.

Suggested fix

1. Ship the props in build/ rather than buildTransitive/. This is the smallest change and it makes the mechanism match the documented rule exactly: the aliases apply to the project that references the package, and nowhere else. A consumer that wants the aliases references the package, which is already what "one alias package per project" tells them to do. The transitive explosion then cannot happen at all.

Worth checking against the case that presumably motivated buildTransitive: a project that references an alias package and is itself referenced by another project that also wants aliases. Under build/ the second project declares its own reference, which seems right rather than a regression.

2. If buildTransitive has to stay, make the second one an error that names itself. Have the props record the storage type it bound and fail loudly when a different one is already recorded:

<PropertyGroup>
  <SemanticsQuantitiesAliasStorage Condition="'$(SemanticsQuantitiesAliasStorage)' == ''">Precise</SemanticsQuantitiesAliasStorage>
</PropertyGroup>

<Target Name="SemanticsQuantitiesAliasConflict" BeforeTargets="CoreCompile"
        Condition="'$(SemanticsQuantitiesAliasStorage)' != 'Precise'">
  <Error Code="SEM010"
         Text="Two storage-type alias packages reached $(MSBuildProjectName): '$(SemanticsQuantitiesAliasStorage)' and 'Precise'. Only one storage type can bind per project. Add PrivateAssets=&quot;all&quot; to the alias PackageReference in the projects that wrap each storage type." />
</Target>

One message naming both packages and the remedy beats 660 CS1537s against a generated file. Do not make it first-wins silently — that would compile with the wrong storage type, which is worse than failing.

3. Document PrivateAssets="all" regardless, in the package description and in CLAUDE.md, as the required pattern for a project that wraps one storage binding for others to consume. That is the shape any multi-storage consumer ends up with.

Related

#236 is the other place the alias packages' relationship to the core is under-specified — there, precision.json cannot express a package-provided storage type. Both come from the same seam: the satellite packages are the supported way to pick a storage type, and the core does not model that.

Activity

  1. matt-edmondson commented on Sep 16, 2026

    @matt-edmondson
    ContributorAuthor

    Triage

    Category: Bug
    Priority: High
    Area: Semantics.Quantities.{Double,Float,Decimal,Precise} — alias props packaging

    High: it breaks the build outright, and it breaks it for the scenario the four alias packages exist to enable. A consumer that wants to compare storage types — which is the documented purpose of shipping four of them — gets 220 × (n−1) instances of CS1537 against a generated file they did not write, with nothing in the error naming a package, and often in a project that references no alias package at all. The documented rule ("one alias package per project") is satisfied by every project in the repro, so a reader following the docs has no way to reach the cause from the symptom. That gap between "the error you get" and "the thing that is wrong" is what makes this worse than a normal build break.

    A workaround exists and is already in use downstream (PrivateAssets="all"; ktsu-dev/Osculator#2 records it as applied), which is what keeps this off Critical.

    Suggested assignment: none specific.

    Related: #236, as the issue notes — both come from the same seam, where the satellite packages are the supported way to choose a storage type and the core does not model that. Not a duplicate; they want separate fixes, but a single decision about what the alias packages are would inform both.

    Open PR covering this: none.

    Suggested next step: suggested fix 1 (ship the props in build/ rather than buildTransitive/) is the one to evaluate first, since it makes the mechanism match the documented rule exactly and removes the failure mode rather than diagnosing it. The one thing to establish before committing: find the case that motivated buildTransitive in the first place. If it turns out nothing depended on transitive flow, this is a small change; if something did, fix 2's self-naming error becomes the fallback — and the issue is right that first-wins-silently must not be the resolution, since compiling against the wrong storage type is worse than failing to compile. Fix 3 (documenting PrivateAssets="all") is worth landing regardless of which of the two wins, and could go in now.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

bugSomething isn't working

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions