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

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

Metadata

Metadata

Assignees

No one assigned

    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