Breaking Changes in C#
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 useTryGetValueEx
(no logger) - in version n+1,
TryGetValueEx
(no logger) is obsolete and users are told to useTryGetValue
(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).- in version n,
- 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.