Ensuring the correctness of your API

Standard

Designing and maintaining an API is hard. Whether you’re distributing assemblies, creating NuGet packages or exposing services (such as WCF, SOAP or REST), you must pay attention to the contracts you provide. The contract must be well designed for the consumer and must be stable over time. In this article, we’re going to see some rules to follow, tips to help and tools to use to ensure that your APIs are great to use.
Although this post is applied to the .Net environment, most of it can be used with other environments (Java, Node, Python, etc.).

Versioning

Why versioning an API ?

The first thing to do for an API is to version it. This way, you can show the evolution to your consumers. But don’t be mistaken, a version is not just random numbers, it’s also a contract that you pass with the consumer about what has changed in your API.

Sometimes, we can see “timestamp” version such as 2018.04.24. While providing the information of when the API was built (or at least revised), the information is not very useful. Consider that you’re running the version 2018.03.25, you expect that in few days, few changes were made. In fact, you can also have a huge big-bang in the API.

By corollary, if you’re using the version 2011.01.01, you could expect that updating for the new API will take you time (7 years of changes, wow!). But the change can be only one method added.

Semantic versioning

In order to avoid those issues, a meaningful way to version is to use semantic versioning (see semver.org).
Using semantic versioning, you provide information to your consumer to help him understand the changes to your API.

It is based on the concept of breaking change and each number in x.y.z has a precise meaning.

  • x is the major, must be updated in case of breaking change
  • y is the minor, must be updated in case of non breaking change
  • z is the patch, must be updated when making bug fixes

But how can we distinguish if a change is breaking, non breaking or just a bug fix ?

A simple rule of thumb is : if the code using your API must be changed when upgrading, then it is breaking.

Note : a new major doesn’t mean you WILL have breaking changes in your code, it means you MIGHT have some. Sometimes, the major is increased just for “marketing” purposes or because there were a lot of features added.

Examples of classification

Here are some changes that are breaking:

  • Removing a public method
  • Removing a public class
  • Removing a public property
  • Removing a parameter to a public method/constructor
  • Adding a parameter to a public method/constructor
  • Making a public member non-public (equivalent do deleting this member)

The changes that are non breaking :

  • Adding a public class
  • Adding a public method
  • Adding a public constructor (only if there was already a declared constructor)
  • Adding a public property
  • Making a non-public member public (equivalent to adding this member)

And the changes that are patches :

  • Any change that has a lower visibility than public (internal, private, etc.)
  • Changes in the implementation of a method/property

Note : Adding a property can be breaking if it is expected to be required (in cases such as SOAP services)

With these examples in mind, it’s pretty easy to understand that only changes to public members are taken in account for major/minor. Therefore, if you don’t want want to be bothered when changing a member, don’t expose this member (make it internal, private, etc.). In fact, when designing an API consider making by default everything not visible and, by exception, only on the things you do want to expose, make them public.

Backward compatibility of an API

When speaking of libraries (assemblies, jar, etc.), there are three kinds of compatibility :

  • With source compatibility, you expect that a new compatible version of the API (new minor or patch) keeps the source code compiling. No change is required.
  • With binary compatibility, you expect that a new compatible version of the API (new minor or patch) keeps the application running. Without recompiling.
  • With behavioral compatibility, you expect that a new compatible version of the API (new minor or path) the application keeps the same behavior (according to defined criteria).

Of course, in the case of a bug fix, and therefore a patch, it might break the behavioral compatibility. For example, a method was returning a wrong value (such as 1+1=3) and was fixed (now 1+1=2) which breaks the behavioral compatibility. In this case it is accepted as long as the previous behavior is considered buggy. But keep in mind that a consumer might have found a work-around on his side and it might provoke regression. It is worth documenting it.

.Net Assembly versioning

Dealing with the binary compatibility in .Net can be tough. Usually in a .Net assembly, you can find a file AssemblyInfo.cs (starting with .NET Core/Standard, this file is auto-generated).
It looks roughly like this :

using System;
using System.Reflection;

[assembly: System.Reflection.AssemblyCompanyAttribute("MyCompany")]
[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")]
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")]
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0")]
[assembly: System.Reflection.AssemblyProductAttribute("MyProduct")]
[assembly: System.Reflection.AssemblyTitleAttribute("MyAssembly")]
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")]

We can see three different assembly attributes for versioning : AssemblyFileVersion, AssemblyInformationalVersion and AssemblyVersion. They have different purposes.

AssemblyInformationalVersion is as its name lets it hint is purely informational. You can put almost anything inside as explained on MSDN:

Although you can specify any text, a warning message appears on compilation if the string is not in the format used by the assembly version number, or if it is in that format but contains wildcard characters. This warning is harmless.

AssemblyFileVersion is the version used by Windows when you’re looking at the properties of a .dll file in the explorer.

AssemblyVersion is the real version used by .Net when checking versions.

When you have unsigned assemblies, and therefore no strong name, you can replace any assembly by any version of the same assembly, .NET won’t prevent you to do so. However, if the assembly you are replacing is not binary compatible, you might end with a MissingMemberException.

When using signed assemblies, the rules tighten. .Net checks that the strong name of the assembly is the same. As a reminder, a strong named is composed by the assembly name, its version, its culture and its cryptographic signature. Here is the strong name of a .NET framework assembly : System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089

How does Microsoft version its APIs ?

Let’s take a look on what is done in the framework by inspecting deeper the System.Core assembly of .NET 4.7.2 :

[assembly: AssemblyTitle("System.Core.dll")]
[assembly: AssemblyProduct("Microsoft® .NET Framework")]
[assembly: AssemblyFileVersion("4.7.3056.0")]
[assembly: AssemblyInformationalVersion("4.7.3056.0")]
[assembly: AssemblyVersion("4.0.0.0")]

First, we can see the AssemblyFileVersion and AssemblyInformationalVersion are set to 4.7.3056.0 which means .NET 4.7.2 (in preview in Windows 10 Insider). Then the AssemblyVersion is set to 4.0.0.0. Wait, what ? Why not the same version that the one in the two other attributes ? The answer is for servicing, updates and this kind of issues.

In fact, Microsoft would like to be able to update the assemblies of the framework and replace them with a new version at every framework release as they ensured that they are binary compatible. As seen above, it is impossible to replace an assembly with a different strong name, so they must “hack” the AssemblyVersion by only putting the major value and the others to zero. As long as they deliver a 4.x version, it will work.

Juggling with BindingRedirects in .Net

Luckily, there is a tool to soften the rules. Often seen but little understood, binding redirects are made for the case when you want to update the version of an assembly with a newer one but the AssemblyName doesn’t match. It’s a tool on the consumer side, and allow him consciously to bypass the rules. In the final configuration file of the application, those few lines serve this purpose :

<dependentAssembly>
  <assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
  <bindingRedirect oldVersion="0.0.0.0-9.0.0.0" newVersion="9.0.0.0" />
</dependentAssembly>

Here, any assembly referencing a version of Newtonsoft.Json between 0.0.0.0 and 9.0.0.0 can be redirected at runtime to an assembly having the version 9.0.0.0. By bypassing the rules, the consumer acknowledge that the versions are binary compatible (or at least that he understands the risks).

With the recent versions of .Net, the framework can automatically generates binding redirects needed when using NuGet packages, in the application config file.

REST services versioning

Versioning REST API is quite similar although it can be done in multiple way.
The easiest and most common is to include the major version in the resource url, such as http://my.great.api/v1/resource . Releasing new minors or patches doesn’t change the url but a new major will.
Same remarks apply for the schema of the resources that are sent or received.

Recommandations about versioning

In my opinion, versioning like Microsoft does is the good way of doing. I usually either use only the major (x.0.0.0) or, my favorite, major and minor (x.y.0.0) for AssemblyVersion. For AssemblyFileVersion, I use x.y.z.b where b is the build number autogenerated by my build factory (TFS/VSTS, TeamCity, etc.).

Breaking changes in the real world

While, most of the time, people agree on the above, in practical it is harder than planned.
The first issue is that it is hard to track all the changes we make to an API and therefore sometime, through a refactoring it’s possible to introduce a breaking change without noticing. This can be solved by the tooling, we will talk about it in the last section of this article.

The second issue is finding ways to avoid breaking changes. A great way to train about this topic is to practice katas using the Baby Steps constraint. We will see some examples on how to deal with common situations.

Avoiding breaking changes

Deleting a parameter in a method

This one is usually easy to deal with : overloads !

public void DoSomethingUseful(int a, int b)
{
    //Do something with those integers
}

If we want to remove a parameter such as b, instead of removing it, we can deprecate the method and overload with the new signature.

[Obsolete("Will be removed in a future version")]
public void DoSomethingUseful(int a, int b)
{
    DoSomething(a);
}

public void DoSomethingUseful(int a)
{
    //Do something with that integer
}

New parameter in a method

public void DoSomethingUseful(int a)
{
    //Do something with that integer
}

The solution here is the same as above although, it can be slightly more complicated. In fact, providing a default value to call the overload is not always an easy task and must be well thought.

[Obsolete("Will be removed in a future version")]
public void DoSomethingUseful(int a)
{
    const int defaultValue = 42;
    DoSomething(a, defaultValue);
}

public void DoSomethingUseful(int a, int b)
{
    //Do something with those integers
}

Changing a name (method, property)

You realized after the first release a typo in a property/method or that the name doesn’t follow your convention. Unfortunately, you’re stuck with it till the end of life of this major version !
However, it’s still possible to have a correctly spelled member.

public void DoSomefingUzeful(int a)
{
    //Do something with that integer
}

Becomes

[Obsolete("Sorry guys, I was drunk while coding this method")]
public void DoSomefingUzeful(int a)
{
    DoSomethingUseful(a);
}

public void DoSomethingUseful(int a)
{
    //Do something with that integer
}

You can see in the three cases above that the deprecated members are just wrappers around the valid members. The logic must not stay in the obsolete member, only the “proxying” logic.

Removing something

Sorry, nothing to do here, just put an [Obsolete] on the next release (minor or patch), you’ll give time for consumers to adapt. Obviously, when deprecating something, you should offer a valid replacement.

Breaking changes in webservices

When introducing breaking changes in web services (whether it is REST or SOAP), you must offer the new endpoints side by side with the old ones during a sufficient period of time. Then you have to track the clients that are slow to migrate, that’s why identifying the calling clients with API keys or similar solutions is interesting.

Chase the late adopters of your API

What is a good API ?

A quick tour about what is a good API with few points. The list is, of course, not exhaustive.

API Design

First of all, think the API you offer, think it as a product. In the same way you would hire designer to create a end-user product (new phone, car, etc.), an API must also be designed.

Quite often, the API is designed after the business code is written and you end up with an API reflecting your implementation of the business, no matter if it has a meaning to the consumer. The API must be centered on the use cases of the consumer and if it doesn’t match with your implementation then an adaptation layer is necessary. Designing the API before implementing anything is recommended.

Use your API

Try to consume it ! It is the best way to know the affordability. Having troubles consuming your API ? Imagine someone not familiar with it.

Naming matters

You should already do it in your code but naming things appropriately is very important. Once again, names must be obvious for the consumer, not the implementer.

Document your API

Even if all your names are meaningful, documentation is very important. Swagger/OpenAPI for example is a great way of documenting REST APIs.

Be useful

Your API must serve a purpose and it must do it well. It does not need to do more (nor less). If you have things that are not relevant to the purpose of your API, hide it as an implementation detail. Also don’t try to put the world in a single API, the purpose won’t be clear (no, having everything in one single API is not a purpose), the maintenance will be complicated and the usage even worse. Think modular and granular.

Avoid dependencies

As much as it is possible. Don’t be that guy who writes a NPM package for upper casing a word and takes more than +1000 dependencies to do that.
Use only the dependencies absolutely needed. If your package starts to reference more and more dependencies, think about exploding it into a core module with extension modules. Each extension having its necessary dependencies, the core almost none.

Stability

Of course, as seen above, don’t break things without warning. And even when breaking, think twice before offering a totally different API that has nothing in common with the previous version. Also, don’t push a major every time, it might take time to integrate.

Joshua Bloch, Principal Software Engineer at Google, made a great slide deck that goes deeper (although it is applied to Java mostly). You can find it here

Tools for creating and maintaining an API

Documenting

In the case of a REST API, a well known specification is Swagger/OpenAPI. In a same way that WSDL files documented SOAP Services, Swagger describes the API. Additionally, it is possible to plug a UI that allows to display the document nicely and query it.
You can find a tutorial on the Microsoft documentation to go further with ASP.Net Core

Ensuring compatibility

As said earlier, it is hard to track the changes of contracts in the object you expose. Hopefully, it is possible to test this and integrate this in the continuous integration process.
Using two NuGet packages, ApprovalTests and PublicApiGenerator and a test framework (such as xUnit), one can write non regression tests of the contracts you expose.
The code would look like this :

using System;
using ApprovalTests;
using ApprovalTests.Reporters;
using Xunit;

[assembly: UseReporter(typeof(XUnit2Reporter))]
namespace MyGreatApi.Tests.ApiApprovals
{
    public class ApprovalTests
    {
        [Fact]
        public void TestApi()
        {
            var publicApiSnapshot = PublicApiGenerator.ApiGenerator.GeneratePublicApi(typeof(MyGreatApi.Person).Assembly);
            Approvals.Verify(publicApiSnapshot);
        }
    }
}

This test will take the API assembly, PublicApiGenerator will examine all the public types (those are the API contract) and generate a snapshot of it. Then, Approvals will compare to an approved snapshot of the API existing in the source control. If those two match, the test succeeds. If you have a change in your API (whether it is breaking or not), it will fail. A manual operation is needed, first examine the difference (is it breaking or not ?) and decide if you want to acknowledge it or review your work to make the change not breaking for example.
Coupled with a code review, you should not let any contract change slip through the cracks. It will also help you increment the values of your semantic version.

Conclusion

As introduced, writing an API is hard and is often taken lightly. With this article, you can now understand the issues related to this topic and hopefully you will be able to tackle them in order to give your consumers APIs that are enjoyable to consume. Don’t forget to design well and good luck !

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.