Anatomy of a Breaking Change


I mentioned that latest release of NMoneys included an "unusual" breaking change.

Let's do some forensics on the internals of such breaking change and how easy it is to solve.

Consuming a library

In order to remove noise, I will exemplify the issue with NMoneys with other types that remove all surrounding noise and are focused to reveal the peculiar breaking change.

Let's have a library that exposes a method defined in a type inside the main namespace for the sake of discoverability.
The method takes an argument and returns a type. Those types, however live in a child namespace because they are closely related and it just seems tidy not to have those types in the main namespace since they are focused to a specific scenario.

Such method would look like this in its initial version:

The layout of the files (that mimics the logical namespace layout) looks like this:

A client that consumes the library by referencing the library assembly (good old "add reference" or "nuget reference", it does not matter) and calls such method would look like this:

Note how the client needs to use both namespaces (root and child), since arguments and return values are defined in the child namespace whereas the method "lives" in the root namespace. But the program works:


A compatible change

Let's imagine the library releases a new version, let's say 1.1 for the sake of the argument and to follow the always sensible semantic versioning.

If we were to drop the new version assembly of the library in a location where it could be loaded by the client and run it, we could be to see that it just works without further fiddling:


A not-so-compatible change

Examining the library we can see that the type uses the child namespace and the child namespace uses information defined in the root namespace.
This was pointed out by NDepend as something to avoid, since it resembles a circular dependency, only that, since we only have one assembly, the compiler is kept happy. One way to break such dependency would be transforming the method to an extension method and place that extension method in the child namespace, alongside its arguments and return type.
Doing so, the root namespace remains ignorant about the child namespace and the cycle does not exists anymore.

Breaking badly

If we were to do the same as before: dropping the assembly where the client could load it and run the client again we would be greeted with an exception.


The explanation is obvious: the method is gone from the type. Extension methods are just syntactic sorcery to make them appear like they are defined in a type, but they are not.

Solving it

To solve the issue we just need to recompile the client against the new version of the library.

No a single change needed.

How bad is it?

As one can see, the client is only broken when the dependency is replaced without recompiling the client.

I might have been doing it all wrong all these years, but I have never ever x-copied a dependency without deploying a recompiled version of the client. Not even for "compatible" releases.

So if you are like me and always recompile before deploy. This kind of breaking change is not so pernicious.