An Object Lesson in Binary Compatibility


A riddle for you, friends: When is changing a method from return void to return Something a breaking change?

If you already know the answer, then why hadn’t you told me? Could’ve saved me a fair bit of embarrassment. Ah well, maybe I missed your call.

First, a story

I had the opportunity to contribute to the HtmlTags open-source project[1](#footnote1). They needed some unit test coverage, and I <3 unit testing and wanted to try my hand at contributing to open-source software. As I got my bearings, I got more confident and started to reach beyond the unit test project into refactoring the application code.

That’s when I found the AddStyle and AddJavaScript methods. Their friends returned an HtmlTag, but they returned void. They would be easier to test if they returned an HtmlTag, but that’s not a good justification for changing a method. But they would be more consistent if they behaved like their neighbors, and that is a sufficient justification to consider it.

HtmlTags is a library meant to be consumed by other applications. It was already in use in the wild. Therefore, it was important not to change its API. It had to work like it always had, else we’d cause considerable grief to the developers using our code. I’ve been the consumer of a service that made breaking changes to its API, and I have choice things to say about the team that saddled me with that noise.

I thought really carefully. I was contemplating changing the return type of a public method in an in-use API. But all those uses would be calling it as if it returned nothing, and you can invoke a method without using its return value. This would be just like that. I asked a coder-buddy for advice. We convinced each other: What harm could it do?

What about the riddle?

Here we come to the answer to the riddle. If you’re changing a library that will be called by other applications, then there are many seemingly harmless changes that are actually breaking changes, including changing a method’s return type. When the consuming assembly is compiled, it is built with instructions to find a method named Whatever that returns void, but the changed assembly’s manifest contains only a method named Whatever that returns string. No match.

Computers. They’re so literal.

To fix it, you merely need to recompile the consuming application, but you don’t discover the problem until assemblies are loaded at run-time, an annoying time to discover exceptions. I coded up a simple example that illustrates the problem. Feel free to follow along at home.

See the problem in action

Make two separate solutions, a class library called MessageOutputter and a console application called ConsoleMessager that references MessageOutputter. First, build MessageOutputter with a return void method.

namespace MessageOutputter
{
  public class Outputter
  {
    private string _message = "This is the outputter, version 1.";

    public string Emit()
    {
      return _message;
    }

    public void UpdateVersion(string versionNumber)
    {
      _message = string.Format("I've been altered by the UpdateVersion method. I am version {0}.", versionNumber);
    }
  }
}

UpdateVersion modifies a private field but does not return a value. Compile that solution and put its dll into a lib folder from which you will reference it in ConsoleMessager. Open the ConsoleMessager solution and add a reference to the MessageOutputter.dll. Write a method that uses the MessageOutputter.

namespace ConsoleMessager
{
  class Program
  {
    static void Main(string[] args)
    {
      var outputter = new MessageOutputter.Outputter();
      Console.WriteLine("Calling the outputter:");
      Console.WriteLine(outputter.Emit());
      Console.WriteLine("Asking the outputter to update, then calling it again.");
      outputter.UpdateVersion("Updated 1");
      Console.WriteLine(outputter.Emit());
      Console.Read();
    }
  }
}

The UpdateVersion method is called, not expecting a return value. Build and run the ConsoleMessager.

The ConsoleMessager calls the MessageOutputter for its message.

Return to the MessageOutputter solution and modify the UpdateVersion method to return the message after it modifies it.

namespace MessageOutputter
{
  public class Outputter
  {
    private string _message = "This is the outputter, version 2.";

    public string Emit()
    {
      return _message;
    }

    public string UpdateVersion(string versionNumber)
    {
      _message = string.Format("I've been altered by the UpdateVersion method. I am version {0}.", versionNumber);
      return _message;
    }
  }
}

Now UpdateVersion returns a string. Build the solution and copy its new dll back into the lib folder, and into the Debug or Release folder under ConsoleMessager/bin, so that the ConsoleMessager will run with your new version of the dll. Run ConsoleMessager and you will encounter the error:

Unhandled Exception: System.MissingMethodException: Method not found: ‘Void MessageOutputter.Outputter.UpdateVersion(System.String)’ at ConsoleMessager.Program.Main(String[] args)

MissingMethodException

The MissingMethodException indicates that ConsoleMessager, when looking within the MessageOutputter library, could not find an UpdateVersion method that returns void. Reopen the ConsoleMessager solution. Before you even build, IntelliSense will tell you that UpdateVersion returns a string now. Build ConsoleMessager and run it again, to see that it works successfully.

After rebuilding, ConsoleMessager successfully calls the new MessageOutputter.

Wiser now

No one raked me over the coals—in fact, my intro-to-OSS experience was thoroughly positive, and I can’t wait to do more—but I know I caused some time-consuming inconvenience to my fellow devs. I have now learned that Binary Compatibility is not the same as Source Compatibility. Conflating the two is like saying, “Works on my machine.” I hope this write-up can help you avoid a similar goof.

1 The nice thing about open-source projects is that everyone has the opportunity to contribute. But I had the especially good fortune to have willing help from Josh Flanagan in finding my way through my first OSS pull request.

True Confessions, Public Shaming, and Test-Driven Development