A definitive guide to API-breaking changes in .NET
This one was very non-obvious when I discovered it, especially in light of the difference with the same situation for interfaces. It's not a break at all, but it's surprising enough that I decided to include it:
Refactoring class members into a base class
Kind: not a break!
Languages affected: none (i.e. none are broken)
API before change:
class Foo
{
public virtual void Bar() {}
public virtual void Baz() {}
}
API after change:
class FooBase
{
public virtual void Bar() {}
}
class Foo : FooBase
{
public virtual void Baz() {}
}
Sample code that keeps working throughout the change (even though I expected it to break):
// C++/CLI
ref class Derived : Foo
{
public virtual void Baz() {{
// Explicit override
public virtual void BarOverride() = Foo::Bar {}
};
Notes:
C++/CLI is the only .NET language that has a construct analogous to explicit interface implementation for virtual base class members - "explicit override". I fully expected that to result in the same kind of breakage as when moving interface members to a base interface (since IL generated for explicit override is the same as for explicit implementation). To my surprise, this is not the case - even though generated IL still specifies that BarOverride
overrides Foo::Bar
rather than FooBase::Bar
, assembly loader is smart enough to substitute one for another correctly without any complaints - apparently, the fact that Foo
is a class is what makes the difference. Go figure...
Adding a parameter with a default value.
Kind of Break: Binary-level break
Even if the calling source code doesn't need to change, it still needs to be recompiled (just like when adding a regular parameter).
That is because C# compiles the default values of the parameters directly into the calling assembly. It means that if you don't recompile, you will get a MissingMethodException because the old assembly tries to call a method with less arguments.
API Before Change
public void Foo(int a) { }
API After Change
public void Foo(int a, string b = null) { }
Sample client code that is broken afterwards
Foo(5);
The client code needs to be recompiled into Foo(5, null)
at the bytecode level. The called assembly will only contain Foo(int, string)
, not Foo(int)
. That's because default parameter values are purely a language feature, the .Net runtime does not know anything about them. (This also explain why default values have to be compile-time constants in C#).
This one is a perhaps not-so-obvious special case of "adding/removing interface members", and I figured it deserves its own entry in light of another case which I'm going to post next. So:
Refactoring interface members into a base interface
Kind: breaks at both source and binary levels
Languages affected: C#, VB, C++/CLI, F# (for source break; binary one naturally affects any language)
API before change:
interface IFoo
{
void Bar();
void Baz();
}
API after change:
interface IFooBase
{
void Bar();
}
interface IFoo : IFooBase
{
void Baz();
}
Sample client code that is broken by change at source level:
class Foo : IFoo
{
void IFoo.Bar() { ... }
void IFoo.Baz() { ... }
}
Sample client code that is broken by change at binary level;
(new Foo()).Bar();
Notes:
For source level break, the problem is that C#, VB and C++/CLI all require exact interface name in the declaration of interface member implementation; thus, if the member gets moved to a base interface, the code will no longer compile.
Binary break is due to the fact that interface methods are fully qualified in generated IL for explicit implementations, and interface name there must also be exact.
Implicit implementation where available (i.e. C# and C++/CLI, but not VB) will work fine on both source and binary level. Method calls do not break either.
Changing a method signature
Kind: Binary-level Break
Languages affected: C# (VB and F# most likely, but untested)
API before change
public static class Foo
{
public static void bar(int i);
}
API after change
public static class Foo
{
public static bool bar(int i);
}
Sample client code working before change
Foo.bar(13);