I would like to gather as much information as possible regarding API versioning in .NET/CLR, and specifically how API changes do or do not break client applications. First, let's define some terms:
API change - a change in the publicly visible definition of a type, including any of its public members. This includes changing type and member names, changing base type of a type, adding/removing interfaces from list of implemented interfaces of a type, adding/removing members (including overloads), changing member visibility, renaming method and type parameters, adding default values for method parameters, adding/removing attributes on types and members, and adding/removing generic type parameters on types and members (did I miss anything?). This does not include any changes in member bodies, or any changes to private members (i.e. we do not take into account Reflection).
Binary-level break - an API change that results in client assemblies compiled against older version of the API potentially not loading with the new version. Example: changing method signature, even if it allows to be called in the same way as before (ie: void to return type / parameter default values overloads).
Source-level break - an API change that results in existing code written to compile against older version of the API potentially not compiling with the new version. Already compiled client assemblies work as before, however. Example: adding a new overload that can result in ambiguity in method calls that were unambiguous previous.
Source-level quiet semantics change - an API change that results in existing code written to compile against older version of the API quietly change its semantics, e.g. by calling a different method. The code should however continue to compile with no warnings/errors, and previously compiled assemblies should work as before. Example: implementing a new interface on an existing class that results in a different overload being chosen during overload resolution.
The ultimate goal is to catalogize as many breaking and quiet semantics API changes as possible, and describe exact effect of breakage, and which languages are and are not affected by it. To expand on the latter: while some changes affect all languages universally (e.g. adding a new member to an interface will break implementations of that interface in any language), some require very specific language semantics to enter into play to get a break. This most typically involves method overloading, and, in general, anything having to do with implicit type conversions. There doesn't seem to be any way to define the "least common denominator" here even for CLS-conformant languages (i.e. those conforming at least to rules of "CLS consumer" as defined in CLI spec) - though I'll appreciate if someone corrects me as being wrong here - so this will have to go language by language. Those of most interest are naturally the ones that come with .NET out of the box: C#, VB and F#; but others, such as IronPython, IronRuby, Delphi Prism etc are also relevant. The more of a corner case it is, the more interesting it will be - things like removing members are pretty self-evident, but subtle interactions between e.g. method overloading, optional/default parameters, lambda type inference, and conversion operators can be very surprising at times.
A few examples to kickstart this:
Adding new method overloads
Kind: source-level break
Languages affected: C#, VB, F#
API before change:
public class Foo
{
public void Bar(IEnumerable x);
}
API after change:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Sample client code working before change and broken after it:
new Foo().Bar(new int[0]);
Adding new implicit conversion operator overloads
Kind: source-level break.
Languages affected: C#, VB
Languages not affected: F#
API before change:
public class Foo
{
public static implicit operator int ();
}
API after change:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Sample client code working before change and broken after it:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Notes: F# is not broken, because it does not have any language level support for overloaded operators, neither explicit nor implicit - both have to be called directly as op_Explicit
and op_Implicit
methods.
Adding new instance methods
Kind: source-level quiet semantics change.
Languages affected: C#, VB
Languages not affected: F#
API before change:
public class Foo
{
}
API after change:
public class Foo
{
public void Bar();
}
Sample client code that suffers a quiet semantics change:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Notes: F# is not broken, because it does not have language level support for ExtensionMethodAttribute
, and requires CLS extension methods to be called as static methods.
Namespace Addition
Source-level break / Source-level quiet semantics change
Due to the way namespace resolution works in vb.Net, adding a namespace to a library can cause Visual Basic code that compiled with a previous version of the API to not compile with a new version.
Sample client code:
If a new version of the API adds the namespace
Api.SomeNamespace.Data
, then the above code will not compile.It becomes more complicated with project-level namespace imports. If
Imports System
is omitted from the above code, but theSystem
namespace is imported at the project level, then the code may still result in an error.However, if the Api includes a class
DataRow
in itsApi.SomeNamespace.Data
namespace, then the code will compile butdr
will be an instance ofSystem.Data.DataRow
when compiled with the old version of the API andApi.SomeNamespace.Data.DataRow
when compiled with the new version of the API.Argument Renaming
Source-level break
Changing the names of arguments is a breaking change in vb.net from version 7(?) (.Net version 1?) and c#.net from version 4 (.Net version 4).
API before change:
API after change:
Sample client code:
Ref Parameters
Source-level break
Adding a method override with the same signature except that one parameter is passed by reference instead of by value will cause vb source that references the API to be unable to resolve the function. Visual Basic has no way(?) to differentiate these methods at the call point unless they have different argument names, so such a change could cause both members to be unusable from vb code.
API before change:
API after change:
Sample client code:
Field to Property Change
Binary-level break/Source-level break
Besides the obvious binary-level break, this can cause a source-level break if the member is passed to a method by reference.
API before change:
API after change:
Sample client code: