The Package Identity That Stranded Sixteen Releases
One commit reclaimed a clean package id and deleted the version prefix in the same breath - leaving two identities on the public feed with inverted version ordering and a csproj that cannot build the package it consumes.
Search a package feed for a library and get two results with almost the same name. One is at 1.2.4 and has not moved in years. One is at 0.4.something and is the one the documentation links to. Neither has a description that distinguishes it, neither is marked deprecated, and the higher version number belongs to the dead one. Now choose.
Part 7 followed a rename through a configuration contract. This part follows a different rename through a distribution contract, and the mechanism is the same: a correct decision, executed in a way that left the artefacts behind.
Sixteen bumps in one file
Through the summer of 2019, src\Ntrada\Ntrada.csproj carried a <VersionPrefix> and it moved constantly. The commit history of that one line is a near-perfect release log:
| Commit | Date | Message | Version |
|---|---|---|---|
feeec05 |
2019-07-05 | Updated logs | 1.0.4 → 1.0.5 |
d104c46 |
2019-07-05 | Fixed headers and CORS | 1.0.5 → 1.0.6 |
20633ec |
2019-07-05 | default content type | 1.0.6 → 1.0.7 |
96ddf15 |
2019-07-05 | Fixed content type check | 1.0.7 → 1.0.8 |
eb6b9e1 |
2019-07-08 | Fixed package version | 1.0.9 → 1.1.0 |
1c24b2e |
2019-07-08 | Content-Type GET only | 1.1.1 → 1.1.2 |
ec4a7d3 |
2019-07-08 | Content-Type GET only (again) | 1.1.2 → 1.1.3 |
795f717 |
2019-07-27 | Fixed resource/request/trace id generation | 1.1.6 → 1.2.1 |
9736954 |
2019-08-01 | Hooks | 1.2.3 → 1.2.4 |
Nine of the sixteen. Four version bumps landed on a single day in July; two commits carry the identical message, minutes apart, each with its own release; and one commit's entire message is “Fixed package version”, which is a release cut to correct a release. This is what shipping without automation looks like, and there is something honest about it — every fix reached consumers the same afternoon it was written.
Under what identity? PackageId was Ntrada_, with a trailing underscore, and the RabbitMQ extension shipped as Ntrada.Extensions.RabbitMq_. The underscore is the fingerprint of a package id that was already taken, or that someone was not yet ready to claim.
One commit, four identity changes
On 21 September 2019, 30c99c2 “Updated license, csproj” put that right:
<PropertyGroup>
<Description>Ntrada API Gateway.</Description>
- <VersionPrefix>1.2.4</VersionPrefix>
- <Authors>Piotr Gankiewicz</Authors>
- <Title>Ntrada_</Title>
- <PackageId>Ntrada_</PackageId>
- <PackageTags>Ntrada_</PackageTags>
- <PackageProjectUrl>https://github.com/spetz/ntrada</PackageProjectUrl>
- <PackageLicense>https://github.com/spetz/ntrada/blob/master/LICENSE</PackageLicense>
+ <Authors>snatch.dev</Authors>
+ <Title>Ntrada</Title>
+ <PackageId>Ntrada</PackageId>
+ <PackageTags>Ntrada</PackageTags>
+ <PackageProjectUrl>https://github.com/snatch-dev/Ntrada</PackageProjectUrl>
+ <PackageLicense>https://github.com/snatch-dev/Ntrada/blob/master/LICENSE</PackageLicense>
</PropertyGroup>
— git show 30c99c2 -- src/Ntrada/Ntrada.csproj
In the same commit, LICENSE moved from Copyright (c) 2018 Piotr Gankiewicz to Copyright (c) 2018 snatch.dev, and the RabbitMQ extension lost its entire packaging PropertyGroup — id, title, tags, project URL and <VersionPrefix>1.2.0</VersionPrefix> all deleted at once.
Walk the diff, because four separate decisions are stacked in it.
- The repository moved from a personal account to an organisation.
spetz/ntrada→snatch-dev/Ntrada, everywhere. - The copyright moved from a person to an organisation. Correct and deliberate.
- The package id shed its underscore. This is the right call.
Ntradais the name of the product;Ntrada_was a placeholder, and formalising the project under an organisation is exactly the moment to reclaim the clean identifier. - And the version was deleted rather than carried across. Not decremented, not reset with intent — the line is simply gone from the file, along with everything else in that block.
The consequence arrives in the very next commit of the same day. a75f76d “Updated core libs, configs, rabbitmq context” adds, to every extension's Release configuration:
<PackageReference Include="Ntrada" Version="0.0.*" />
The clean Ntrada id starts at 0.0.x while Ntrada_ sits on the public feed at 1.2.4. It climbs from there — 0.0.* → 0.3.* in 16d19b9 (the .NET Core 3.0 migration, 2019-10-18) → 0.4.* in 5016fdb (the 3.1 migration, 2019-12-20) — and stops.
Two identities, inverted
Here is the finding, stated plainly. The estate has two package identities on a public feed for the same software, with inverted version ordering: the abandoned one numerically outranks the live one. A consumer searching for “Ntrada” is offered a choice between a higher-versioned dead id and a lower-versioned live id, and has nothing in either package's metadata to disambiguate them — because there is no description on the extensions, no repository URL on any of them, no readme and no release notes.
There is no deprecation notice on the old id and no redirect. Both are things NuGet supports and neither was used.
It is instructive precisely because the underlying decision was correct. Reclaiming a clean package id when you formalise a project under an organisation is what you should do. The mistake was cutting the version to zero on a codebase that had already shipped sixteen releases under the old name, and then leaving the old name to sit there outranking you forever.
There is a rule in here worth taking away: a package id is a permanent public identifier and its version is the only continuity signal a consumer has. If you change the id, carry the version; if you cannot carry the version, deprecate the id you left. One of those two, always.
The csproj that cannot build the package it consumes
Look at src\Ntrada\Ntrada.csproj at HEAD and there is no <Version>, no <VersionPrefix>, no <PackageVersion> — not in that file and not anywhere else in the repository. Run dotnet pack on it and you get Ntrada.1.0.0.nupkg, because 1.0.0 is the SDK default.
Meanwhile all six extensions declare <PackageReference Include="Ntrada" Version="0.4.*" />.
Nothing in this repository can produce the package this repository consumes. Whatever was on the feed as Ntrada 0.4.x was packed from a working tree whose version came from somewhere the repository does not record — a -p:Version= on a command line, in a shell, on one machine.
That is not a hypothetical inconvenience, because there are no git tags in this repository at all, no CHANGELOG, no GitHub releases referenced from the README, and no pack or push step anywhere in the CI workflow — no dotnet pack, no dotnet nuget push, and not a single secrets. reference of any kind, which is the mechanical proof that every package on the feed was pushed by hand. Reconstructing “which commit is Ntrada 0.4.3?” from this repository is not merely hard; it is not possible.
The licence that never ships
One more line in that PropertyGroup deserves its own paragraph:
<PackageLicense>https://github.com/snatch-dev/Ntrada/blob/master/LICENSE</PackageLicense>
PackageLicense is not a NuGet pack property. The real ones are PackageLicenseExpression (MIT, here), PackageLicenseFile, and the long-deprecated PackageLicenseUrl. PackageLicense is silently ignored by the SDK's pack targets.
So the MIT licence — correctly worded, correctly attributed, sitting at the repository root — does not travel in the .nupkg. And the five extension packages carry no metadata whatsoever: no PackageId, no Description, no Authors, no project URL, no licence. To a consumer's software-composition-analysis tooling, all six Ntrada packages read as unlicensed dependencies, which in a regulated shop is an automatic block on a project whose actual licence is as permissive as licences get.
To be fair, and this one deserves it: PackageLicenseUrl was being deprecated exactly in this window — NuGet 4.9, late 2018 — with noisy build warnings pushing everyone to change something, and the correct replacements were newly minted. PackageLicense is a plausible-looking guess made at the precise moment the API was in flux, and it produces no warning of its own. This is a typo-class mistake, not negligence. It is still a finding, because the effect is indistinguishable from having no licence.
What was absent that would have caught all of it
There is no Directory.Build.props anywhere in this repository. Every one of the twelve projects repeats its own TargetFramework and LangVersion, which is exactly why the Swagger project could quietly diverge by omitting LangVersion and the integration test project could quietly retain a 2.2.0 package reference alongside 3.1.0 ones.
One file, ten lines, centralising target framework, language version, version number, authors, licence expression and repository URL for all twelve projects, would have made every finding in this article structurally impossible. It was never written.
Also absent: no NuGet.config, no packages.lock.json, no <RestorePackagesWithLockFile>. Combined with 0.4.* — a floating wildcard consumed by six projects — restoring this repository today does not produce the graph it produced in December 2019. And only the two test projects set <IsPackable>false</IsPackable>, so a dotnet pack at the solution root would happily emit Ntrada.Host.1.0.0.nupkg and two sample-application packages alongside the real ones. Nothing in CI runs pack, so it never fired — but that is luck, not design.
The fairest summary of this estate's packaging, and it is the one I would put in a review: Ntrada gets the hard part of library design right and the easy part wrong. The extension contract from part 2 is a better plugin model than a lot of production frameworks of its era shipped, and it is why six packages can be as small as they are. What it gets wrong are the mechanical, checkable things — a version number, a licence expression, a props file, a pack-and-push job. Those are the parts a teaching repository is most often forgiven for and least able to afford, because a reader copies the csproj at least as readily as the interface.
Next, the reason CI never noticed any of this — and the project with no tests in it that turns out to be holding the estate up.