Breaking Changes in C#

1/18/2019 - Marco von Ballmoos (updated on 9/18/2020)

Due to the nature of the language, there are some API changes that almost inevitably lead to breaking changes in C#.

Change constructor parameters

While you can easily make another constructor, marking the old one(s) as obsolete, if you use an IOC that allows only a single public constructor, you're forced to either

  • remove the obsolete constructor or
  • mark the obsolete constructor as protected.

In either case, the user has a compile error.

Virtual methods/Interfaces

There are several known issues with introducing new methods or changing existing methods on an existing interface. For many of these situations, there are relatively smooth upgrade paths.

I encountered a situation recently that I thought worth mentioning. I wanted to introduce a new overload on an existing type.

Suppose you have the following method:

bool TryGetValue<T>(
  out T value,
  TKey key = default(TKey), 
  [CanBeNull] ILogger logger = null

We would like to remove the logger parameter. So we deprecate the method above and declare the new method.

bool TryGetValue<T>(
  out T value, 
  TKey key = default(TKey)

Now the compiler/ReSharper notifies you that there will be an ambiguity if a caller does not pass a logger. How to resolve this? Well, we can just remove the default value for that parameter in the obsolete method.

bool TryGetValue<T>(
  out T value,
  TKey key = default(TKey),
  [CanBeNull] ILogger logger

But now you've got another problem: The parameter logger cannot come after the key parameter because it doesn't have a default value.

So, now you'd have to move the logger parameter in front of the key parameter. This will cause a compile error in clients, which is what we were trying to avoid in the first place.

In this case, we have a couple of sub-optimal options.

Multiple Releases

Use a different name for the new API (e.g. TryGetValueEx à la Windows) in the next major version, then switch the name back in the version after that and finally remove the obsolete member in yet another version.

That is,

  • in version n, TryGetValue (with logger) is obsolete and users are told to use TryGetValueEx (no logger)
  • in version n+1, TryGetValueEx (no logger) is obsolete and users are told to use TryGetValue (no logger)
  • in version n+2, we finally remove TryGetValueEx.

This is a lot of work and requires three upgrades to accomplish. You really need to stay on the ball in order to get this kind of change integrated and it takes a non-trivial amount of time and effort.

We generally don't use this method, as our customers are developers and can deal with a compile error or two, especially when it's noted in the release notes and the workaround is fairly obvious (e.g. the logger parameter is just no longer required).

Remove instead of deprecating

Accept that there will be a compile error and soften the landing as much as possible for customers by noting it in the release notes.

Sign up for our Newsletter