Working with services and all that scaffolding tools is very convenient. You can setup a new WebApi within minutes. Define your data classes and let EntityFramwork take care of all the ugly stuff, you don’t want to deal with. That’s fun and works pretty well, as long as you have full control over your data classes, the databases and your services.
But what if your code makes it into the wild? The data classes are released to the public or to your customers. The code is in production, your customers and third-party vendors heavily rely on your APIs and data classes. Sooner or later requirements change, and you’ll need to adjust your code. Worst case the changes are breaking your APIs and your previously carefully crafted client libraries don’t work with the new APIs. So, how can you ensure, that all your APIs, services and the clients connecting to these services get updated and can talk with each other without problems? The short answer is, you can’t, if you don’t design the ability to handle changes upfront into your model!
It’s all about the correct version
There are several ways to handle API changes. One way is to keep the old API up and running by adding additional routes for older APIs (API-Gateway). Of course, the business logic inside your (old) services needs to adjust to the new data structures. That might introduce more complex branching strategies within your source control, as you now have a new API which directly uses the latest data structures, and you’ll still need the old API, which must transform old data structures into the new structures before processing it with the business logic. The result might need to be transformed again back to the old data structure.
This way of dealing with different versions is a combination of a codebased solution (you need to write code for the transformations) and a solution within the infrastructure (you need a routing mechanism for the different API versions). The nice thing about a solution within the infrastructure, is that you can use all the scaling mechanisms an infrastructure offers. If your infrastructure is based on microservices, just run your APIs in different microservices and let K8s scale the services up and down. Nice!
The routing can also be handled within your code. When thinking about .NET WebApi solutions, it’s just a matter of adding a controller for the new and a controller for the old API version, and decorate them with the appropriate route.
Versioning within the domain
Another way of dealing with different versions is to model the version into your domain classes. This is useful, when you don’t have the possibility to separate your APIs via the infrastructure.
We’re making some assumptions here, so your needs may be different, but the solution can be adjusted easily.
First, we assume, that we don’t have any breaking changes between our API versions, so we are always able to convert our entities from one API version to another. A conversion might lose some information, but we’re assuming that we have well defined defaults.
Second, we assume that our domain entities all use the latest data structure, and we’ll just need backwards-compatibility to support clients using older APIs and not the other way round.
We’re introducing a new class called
Version. This class is a technical class but can also play the role of an entity among your domain models. We’ll also introduce representation classes for each domain entity and version (i.e. DTOs). We also need a conversion logic for all DTOs between the existing versions (adapter pattern).
The key question is now, how does the service know, what version to use? A naive way to accomplish this is to enhance the business logic explicitly by always providing the expected version in all method signatures. So, instead of requesting
GetPersonById(23) our method needs an explicit specification of the expected version like
GetPersonById(23, "1.0"). There are several reasons, why you shouldn’t do this: first of all, it messes up with your domain logic. All methods, even the simplest ones are scattered with version parameters. This makes your code less intuitive. Another reason is that all developers have to remember to always include the version parameter in new methods. It’s easy to forget about that.
However, there is the chance, that the version class, which we introduced as a helper class, has a real meaning inside of your business domain model. For example, think of a car manufacturer having each year a new version of his car model, like the BMW 316, which got a lot of small changes or face liftings over the years, but ways always kept under the same name “316”, but with different versions.
Metadata or how do I serialize
A more common option to overcome the versioning issue, is treating the above introduced DTO classes a different representation of the same data. This means, the way of representing the data, is just a matter of serialization.
To specify how the data is sent over the wire, the client must indicate the serialization it understands. This can be done, by sending information within the headers, similar to the
accept header for the MIME type or the
accept-language headers defined in the http protocol.
Unfortunately, http does not specify a header for version information. So, you’ll end up with you own header definition and interpretation of that header. A typical header could be set like this:
This information can be evaluated to invoke the corresponding adapter classes before and after the request gets handled by the business logic.
This kind of interception can be easily done in the pipelining implementation of APS.NET MVC or WebApi. Just provide a filter class, in which you check for our custom
x-accept-version header and setup the adapters accordingly.
One drawback here is that the version is not bound to the endpoint anymore. So, there’s no way to retrieve the schema information for the endpoint, because the schema can be determined only at the time of the request. And the schema may change with each request.
Another drawback is the behaviour, when no version information was sent by the client. Assuming that in such case the latest version is always used, might fail, depending on the distribution of different clients (old and new), which are in use. Falling back to the minimum version might not be the best decision also. The minimum version doesn’t contain your latest changes, so there will be some information loss. Without a warning, about the data loss, errors can stay uncovered as everything seems to be working well.
I hope you enjoyed reading this article about versioning.
Stay tuned and happy coding 🙂