Semantic versioning APIs of .NET assemblies

Some time ago I wrote a post on this topic, and decided it was time for an update.


  • Package managers are awesome.
  • Having many small libraries is good design.
  • Updating libraries in a complex dependency graph is annoying.
  • Throwing away compile time safety (i.e.using binding redirects) is not the answer.
  • Setting the .NET assembly version the same for assemblies with compatible APIs can alleviate this pain.
  • Let’s automate it!

I also previously described a way to version your versioning system, which is fortunate, because I’ve decided it’s due an upgrade.

Why change the old system?

Firstly, the old system didn’t reward efforts to avoid source-level breaks, i.e. ensuring dependent projects don’t need to change their source code even if they have to recompile. This means changes are either saved up, and then really painful all at once, or a constant stream of breaking changes become the norm. Even figuring out what those cases are needs careful thought to get right, and automatically detecting them needs a lot more effort.

The second reason is more fluffy. There’s something slightly unsettling about a library’s major version increasing rapidly. However many iterations you go through, that version never goes back down, and it just looks weird.

New system

The new system has a Nuget Package version of Major.Minor.Patch and an assembly version of Major.Minor.0.0. The assembly file  version can still be whatever you want, but Major.Minor.Patch.0 seems sensible to me. Here’s the rules for when to increment those version parts:

  • Increment Patch when the API is a superset of build with a compatible API
  • Increment Minor when the API has changed in a way that breaks source compatibility (requires recompilation), but is unlikely* to require source code changes to compile.
  • Increment Major when the API has changed in a way which requires recompilation and is likely* to require source changes for consuming code to compile.

* The definitions of likely and unlikely are a bit flexible here and I’d recommend erring on the side of caution. I would define it as “there is no common coding pattern for consuming the changed member which will be broken by the change”. I would like to refine this over time, but here are a few examples of what I’m thinking about.

  • Patch: Nothing changed at all, or only internal/private things changed.
  • Patch: All the existing public members remain and some more were added.
  • Patch: A default parameter’s value has changed. Default parameters are inlined at compilation time, so it’s not part of the method signature. However, depending on the change, it may be worth making this a minor/major version bump since it’s changing behaviour in a subtle way.
  • Minor: A public class now implements an interface that it didn’t previously.
  • Minor: A public static method now returns type T instead of interface I where T implements I.
  • Minor: A public method has turned into an extension method in the same namespace. It’s possible there’s an extension method with the same name in consuming code, but that seems unlikely.
  • Major: A public method now returns a long instead of an int. Some uses could still work, but if a calling method doesn’t use “var”, or returns the variable itself recompilation would be needed. This seems likely.

This encourages people to make changes in a backwards compatible way without completely setting the API in stone, and means that consumers get a grace period to react to things becoming obsolete.

Consuming packages

A library which itself has dependencies, should regularly update to LatestMajor.LatestMinor.0. This is the least constraining for any consumers since it means a consumer is free to use any patch version from 0 to LatestPatch, rather than being forced to upgrade for every single bugfix.

An application at the top of the dependency chain is able to update as often as it likes, to any version, though it will find that only certain ranges of dependencies are compatible with its other dependencies. I’d recommend regularly updating everything to the absolute latest version.

What I aim to do for my own libraries in future, is have them automatically build when a dependency releases a new major/minor version, attempt the update to LatestMajor.LatestMinor.0 and commit it back to source control if it builds and passes the tests. On TeamCity for example, it should be possible to just do the update/commit as a build step that commits to a branch called dep/{name}/{version}, and have TeamCity auto-merge branches matching dep/* that pass the tests.

Setting the version automatically

For the old scheme from the previous blog post, I had a tool to detect what the semantic version should be. I am currently rewriting that tool to work under the new scheme (using Roslyn). At first, everything will just be either a major, or a patch bump. Ideally there’d be an easy manual override to classify something as a minor version. This would allow it to be used for a custom scenario like “No-one actually ever calls this method – I’ve checked”. Over time, I hope to add detection of common changes which should only increase the minor version. I also intend to build other tools to help make this scheme very convenient, for example storing a summary of the public API making it easy to see changes over time.

You can follow my progress at:




About GrahamTheCoder

I'm Graham (the coder). A C# developer based in Cambridge hoping blogging will achieve some or all of the following. Help me organise my thoughts. Practice writing things other than code. Give me a place to refer people to when I'm trying to make a long winded point. I welcome comments and constructive criticism, and hope to look back at my written opinions in the future and laugh at my own naivety.
This entry was posted in C# and tagged , , , , , , . Bookmark the permalink.

One Response to Semantic versioning APIs of .NET assemblies

  1. Pingback: What’s in a (strong) name? | Crafting code in C#

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s