This blog post will focus on source incompatibilities.
Part 1 was all about a simple, but hard to follow, rule: “don’t make changes to your library that alter its behavior”. That was about it, just don’t do it!
Part 2 and 3 are about additional types of backward compatibility that you may want to guarantee to your customers. You don’t have to, many products don’t, but you should decide in advance and set expectations accordingly.
Source Incompatibilities
This next type of incompatibility is the most straightforward: when your customers update your library, their projects don’t compile anymore.
This is obviously annoying as your customers now have to scramble to update their code. On the other hand, this is way better than a silent behavioral change in your library: source incompatibilities never go unnoticed! We have already discussed in the previous post how you can even use the Obsolete attribute to force a source incompatibility and protect your users from a behavioral change:
[Obsolete("This class is obsolete, use SeverelyBuggedClassV2 instead.", error: true)]
public class SeverelyBuggedClass
{
}
Name Conflicts
If your goal is to never break source compatibility, be advised that you won’t be able to completely guarantee that. At the very least, you don’t have a way to control which type names are already used in your customer project and you may end up with a conflict when adding new classes to your library.
This is not the end of the world as the resulting error is straightforward and easy to fix:
Error CS0104 'Foo' is an ambiguous reference between 'Namespace1.Foo' and 'Namespace2.Foo'
Addressing this error may be extremely tedious and time consuming though. At the very least, make sure not to use names conflicting widely used .NET types from Microsoft (e.g., don’t name your class Int32
or String
).
An interesting corner case is when a new method signature (name and parameter types) conflicts with an extension method defined by your customer. This is actually a behavioral incompatibility because it won’t result in a compilation error but will silently switch your customer code to use the new method instead of the extension method!
Common Source Incompatibilities
Most types of source incompatibilities are pretty evident:
- Renaming or removing a type, property or method
- Removing
virtual
from a method - Adding
final
to a class - Changing a method, property or field to be
static
or non-static
- Adding a constructor with parameters to a class without constructors
- Making
public
types internal
- Making
public
or protected
members private
or internal
- Adding non-optional parameters to a method
- Changing method parameter types (unless a implicit conversion is available, e.g., changing
short
to long
is ok) - Changing property, field and method return types (unless a implicit conversion is available, e.g.,
long
to short
is ok) - Changing method parameters modifiers (in, out, ref or removing params)
- Renaming a method parameter (this breaks the usage of named arguments)
- Adding type constraints on generic types
- etc.
Stop!
Go back and read the list again, I am sure there are a couple of entries you have overlooked.
(I know I had to go back and add to the list multiple times while writing the post…)
Photo by Mollybob, used under Creative Commons license.
Making any of these changes is only an issue for public
members of public
types (or protected
members of public
non-final types): if your customer can’t use what you have changed, you won’t break them. There is an exception to this rule: reflection.
Reflection
Reflection allows to access types and members that would normally not be visible. This violates all encapsulation rules and, unless you have instructed your customers to use reflection on your library, is a very bad practice.
You may want to be explicit in your documentation and state that all private portions of your library can be changed without notice and without any backward compatibility guarantee. Except for that, I think most customers who use reflection on someone else’s library know that their code may break.
If you use reflection within your library (there are indeed some reasonable use cases for it), make sure you have unit tests for that code because you may easily break your own library when making changes.
Interfaces and Abstract Classes
While most source incompatibilities come from the removal of features that your customer is using, the addition of constraints is also a problem.
The most common source of this issue is the addition of methods or properties on public
interfaces or the addition of abstract
members to classes. This can easily be overlooked as “adding functionalities” but, if a customer is implementing the interface (or extending the abstract
class), they will now have to change their code to implement the new members.
Implicit Type Conversions
Unfortunately, there are very few tools to work around introducing source incompatibilities. For the most part, it is just a matter of making a good design in the first place and being careful when making code changes later.
One area where we have some language support is changing input and output types for methods, properties and fields. We can define implicit conversion operators to keep the change source compatible.
For example, let’s say that we have this method:
public class Calendar
{
public DateTime FindNextAppointment(DateTime start);
}
and we don’t like how the DateTime
type in .NET can be Utc
, Local
or even Unspecified
, making this method error-prone to use.
We can change its parameter and return type to a different one if we provide implicit conversions:
public class Calendar
{
public UtcDateTime FindNextAppointment(UtcDateTime start);
}
public struct UtcDateTime
{
private readonly DateTime Time;
public UtcDateTime(DateTime time)
{
switch (time.Kind)
{
case DateTimeKind.Utc:
Time = time;
break;
case DateTimeKind.Local:
Time = time.ToUniversalTime();
break;
default:
throw new NotSupportedException
("UtcDateTime cannot be initialized with an Unspecified DateTime.");
}
}
public static implicit operator UtcDateTime(DateTime t) => new UtcDateTime(t);
public static implicit operator DateTime(UtcDateTime t) => t.Time;
}
This change is source compatible (not behaviorally compatible though because we are now throwing a NotSupportedException
).
What Next?
The next blog post will cover the pros and cons of guaranteeing binary compatibility to your customers as well as how to actually maintain binary compatibility for your library.