
Many long-lived .NET repositories did not start with semantic versioning. They grew up with major.minor.build.revision, often set in AssemblyInfo.cs, sometimes mixed with wildcard auto increment, sometimes duplicated across several projects and sometimes quietly diverging over the years.
That model is still valid in the places where .NET and Windows expect numeric version components. The problem is elsewhere: as a public release contract, x.x.x.x is a poor fit for modern package distribution and CI/CD. It does not say much about compatibility and older implementations often tie version identity to build time instead of source state.
That is why a migration to SemVer is not mainly a formatting exercise. It is a change in responsibility. The repository stops treating one four-part number as the universal source of truth and starts separating release identity from binary identity.
This article is the baseline for the two application-specific follow-ups: Modern versioning for ASP.NET Core and web apps and how to produce idempotent artifacts and Modern versioning for .NET desktop apps and how to produce idempotent artifacts . Those two assume the migration is already done and focus only on their platform-specific consequences.
In practice, the target model usually looks like this:
- SemVer becomes the public release and package contract
AssemblyVersionbecomes a conservative binary compatibility markerFileVersionremains useful for Windows tooling and diagnosticsInformationalVersioncarries the richest build metadata
When that split is done carefully, the repository gets clearer release semantics without breaking consumers that still depend on stable assembly identity.
Start by understanding the existing version surfaces
Before changing anything, it is worth finding out what the old scheme is actually doing.
Typical patterns include:
AssemblyVersion("3.4.0.0")AssemblyFileVersion("3.4.125.0")AssemblyVersion("1.0.*")- manually edited
<Version>3.4.125.0</Version>entries in project files - installer versions copied from file versions
These often coexist in one repository. That matters because the migration path depends on who currently consumes what. A public NuGet package, a desktop plugin model and an internal web service do not all have the same compatibility constraints.
The most useful first step is therefore not editing. It is inventory.
The real change is conceptual, not cosmetic
The core idea is easy to miss: moving to SemVer does not mean numeric assembly versions disappear. It means they stop pretending to be the only version that matters.
A clean migration target normally wants:
- public versions like
3.5.0or3.5.0-rc.1 - a stable
AssemblyVersionacross a compatible major or minor line - file versions that continue to move with shipped binaries
- informational versions that carry source-derived traceability
That is why good migrations usually preserve binary stability more than they change it. The real win is clarity, not novelty.
Step 1: Remove wildcard auto increment first
If the repository still uses the old compiler wildcard model, that should go first.
Examples:
1[assembly: AssemblyVersion("1.0.*")]
or:
1[assembly: AssemblyFileVersion("1.0.*")]
The attraction is obvious: automatic incrementing with almost no setup. The downside is that the generated values are effectively time-based. Rebuilding the same commit later yields a different version even if the code is identical.
For a SemVer migration, that is not a minor cosmetic detail. It is a behavior change that has to happen before the repository can reasonably claim that version identity is source-based.
Step 2: Centralize version ownership
If version metadata is scattered across projects, centralize it before changing the public release model.
At a minimum that means pulling intent out of multiple AssemblyInfo.cs files or per-project version properties and moving it to one place. For repositories that want to stay mostly manual, Directory.Build.props may be enough. For repositories that want Git-driven versioning, this is usually the moment to introduce NBGV.
Trying to adopt SemVer while version state still lives in five different files is a reliable way to create a half-migrated repository.
Step 3: Add NBGV as workflow and build infrastructure
The migration works best when the repository adopts both the CLI workflow and the build integration.
1dotnet tool install --global nbgv
2nbgv install
That gives the repository a version.json file and wires the Nerdbank.GitVersioning build integration into the project. If package management needs to be handled manually, the package can still be added directly:
1Install-Package Nerdbank.GitVersioning
The distinction matters. The CLI is how the repository manages its version workflow. The package is what actually stamps the build output consistently.
Step 4: Define the first SemVer line deliberately
The migration point is where the repository stops saying “build 125 after build 124” and starts saying “this is the 3.5 line”.
1{
2 "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
3 "version": "3.5",
4 "assemblyVersion": {
5 "precision": "major"
6 },
7 "publicReleaseRefSpec": [
8 "^refs/heads/main$",
9 "^refs/tags/v\\d+\\.\\d+(\\.\\d+)?$"
10 ],
11 "release": {
12 "branchName": "release/v{version}",
13 "tagName": "v{version}",
14 "versionIncrement": "minor",
15 "firstUnstableTag": "alpha"
16 }
17}
The important part here is not only version. It is also the explicit assemblyVersion policy. By setting precision deliberately, the repository decides how much of the computed version should flow into CLR-facing identity.
Step 5: Keep AssemblyVersion conservative unless compatibility changed
This is the part that prevents most migration damage.
Teams often break binary compatibility accidentally because they map SemVer too literally onto AssemblyVersion. That is usually the wrong move, especially for libraries and plugin-oriented systems.
A safer default is:
- keep
AssemblyVersionstable across the compatible release line - allow file and informational versions to move with NBGV
- change
AssemblyVersiononly when compatibility actually changed
If the old repository used something like 3.4.125.0 as its assembly version, the right migration target is often not 3.5.0.0 for every subsequent release. In many codebases the smarter move is to stop mirroring public release numbers into assembly binding identity.
Step 6: Replace time-based increment with source-based progression
Most teams still want automatic progression after the migration and that is reasonable. The difference is that the increment now needs to mean something.
In the old model, auto increment often meant:
- next build-time-derived number
- next revision generated from the clock
- next CI counter
In a Git-driven model, it usually means the next version height within the current release line.
That is why NBGV fits this migration well. The human-controlled release line stays explicit, while repository history supplies the unique progression between releases.
Step 7: Move external consumers gradually
If the repository publishes packages or feeds other automation, the migration is almost never just a local refactor. Downstream systems may care about:
- package sorting
- installer version comparisons
- assembly binding behavior
- support tools reading file versions
- dashboards or scripts parsing the old numeric format
That usually means there needs to be a transition period in which:
- SemVer becomes the public package and release identity
- file versions remain numeric and operationally useful
- assembly versions stay conservative
- runtime diagnostics expose both informational and file version data
That transition is often what makes the migration safe in practice.
Step 8: Expose the new identity at runtime
Once the repository moves to SemVer, the software should report that identity clearly instead of hiding it behind old assumptions.
NBGV’s generated ThisAssembly class is useful here:
1namespace BenjaminAbt.Diagnostics;
2
3public static class BuildMetadata
4{
5 public static string InformationalVersion
6 => ThisAssembly.AssemblyInformationalVersion;
7
8 public static string FileVersion
9 => ThisAssembly.AssemblyFileVersion;
10
11 public static string AssemblyVersion
12 => ThisAssembly.AssemblyVersion;
13}
This is particularly helpful during the migration window because it lets applications and support tooling surface both the public SemVer identity and the remaining numeric technical identifiers.
A migration sequence that usually works
In practice, the safest order is usually this:
- inventory all current version surfaces
- remove wildcard auto increment
- centralize version ownership
- introduce NBGV
- add
version.json - define the first SemVer release line
- stabilize
AssemblyVersion - update package, installer and runtime reporting
- cut the first public SemVer-based release
The order matters because it avoids changing compatibility signaling, build mechanics and release workflow all at once.
The mistakes that usually cause pain
The common failure modes are predictable:
- mapping SemVer directly onto every version surface
- keeping time-based auto increment while claiming the repository is now SemVer-based
- changing
AssemblyVersionmore often than before - updating package versions without updating runtime diagnostics
- rebuilding old releases later under a different SDK and treating them as equivalent
Most of these mistakes come from treating the migration as a string rewrite instead of as a repository workflow change.
Conclusion
Migrating from x.x.x.x to SemVer is not about declaring the old four-part format obsolete. Numeric versions still have a valid role in assembly and file metadata. The real goal is narrower and more useful: stop using that format as the primary public release contract.
Once SemVer becomes the package and product identity, AssemblyVersion stays conservative, auto increment becomes source-based and the build is stamped consistently through NBGV, the repository ends up with something much more valuable than a prettier version string. It gets a release model that is easier to reason about and much harder to misapply.
Related articles

Jul 03, 2026 - 7 min read
Modern versioning for ASP.NET Core and web apps and how to produce idempotent artifacts
ASP.NET Core applications are usually released often enough that versioning stops being a documentation concern and turns into delivery …

Jun 30, 2026 - 8 min read
Modern versioning for .NET desktop apps and how to produce idempotent artifacts
Desktop software makes versioning visible in a way that web systems often do not. A backend can hide a surprising amount of release …

Jun 26, 2026 - 9 min read
Modern versioning for .NET apps and libraries and how to produce idempotent artifacts
Versioning stays invisible right up to the point when it fails. A package has to be reissued, a support case depends on one exact binary, or …
Let's Work Together
Looking for an experienced Platform Architect or Engineer for your next project? Whether it's cloud migration, platform modernization or building new solutions from scratch - I'm here to help you succeed.
