Versioning strategies #wcf #nservicebus

Versioning is hard. The first rule of Versioning is don't do versioning, meaning - if we are at all able, we should try to find ways other than versioning components to coordinate upgrades of the same. Let's start there. If you have component that you wish to expose to a third party - a service of some kind - and you resist the temptation of creating a generic interface to that service, but instead create a specific interface for that consumer, you will have little reason to change this interface in the future, when you need to cater to another consumer. I've gone into more details in a previous blog post, so I'll just refer you there.



When you do need to do versioning, however, there are some strategies you can follow to avoid the dreaded breaking change scenario, where one or more of your consumers stop working, until they are in sync with your recent changes. The particular strategy explained below, has been tested with WCF Services (using XML serialization and HTTP transport) and NServiceBus messaging (using JSON serialization and MSMQ for transport). If you'd rather just see code, please see my versioning-strategies repository at GitHub.

Prerequisites - message contracts are artifacts

- A message contract – whether it is used in an RPC fashion or as an event, is an agreement between two parties, governing the communications format.



- Seeing message contracts as artifacts, makes us realize that they represent a resource which both parties are dependent upon.



- As such, they need to be independently versioned and deployed.

And by versioned and deployed, I mean Packaged into a Semantically Versioned NuGet package and deployed to a shared repository (whether that's a shared directory with all historic packages stored, or distributed to a package repository of some kind).

Introducing future breaking changes by using a staggered upgrade approach

When we need to update a message contract, in order to ensure that we don't break clients along the way, we need to separate the update into two distinct parts - add new and remove old.

In the add new phase, we'll add our new properties to our contracts and flag old ones with the [Obsolete] attribute, but we will not remove anything. We will also not muck with the order of the properties in the contract. If our goal is to replace an existing property with another, we will - in this phase - only add the new property, letting the old one remain.

After the add new phase, we will perform a release. Since we have not broken anything (we've added properties), it is safe for both the producer and the consumer to take a dependency on the new artifacts. What is more, is that these type of changes are backwards compatible, meaning we can allow for the producer to take on a new dependency and let the consumer lag behind without anything bad happening (other than the consumer not getting the new information elements, that is).

In the remove old phase, we clean the message contracts of [Obsolete]d properties, presuming that both producers and consumers have already dealt with its implication (from the previous release). This means that producers - on average - only need to remove code once they've taken a dependency on this contract and that consumers - on average - needs to do absolutely nothing.

OK, that was the overview. Let's get into details!


Take a scenario where we have four artifacts in production: Banana Contracts, Banana Service, Monkey Consumer & Tapir Consumer (they supposedly eat bananas ...). In the first release, the teams responsible for the Banana Service and the teams responsible for the Monkey Consumer and Tapir Consumer respectively are able to work together, co-releasing their components which - unsurprisingly - work quite well together.

Monkey Consumer gets a life of its own and take off in the company, getting more and more responsibilities and features. Meanwhile, a critical shortsightedness is found with regards to the Tapir service - it cannot perform its work with the given amount of information, but needs its data described in another fashion - replacing one string property with a new complex type containing a string and an int, of which the consumer needs an array. Disregarding using this as an opportunity to introduce the Isolated Conversion Pattern, the service provider team realizes that it cannot remove the string property from the message contract without breaking its Monkey Consumer. Presuming that we cannot shut down the Monkey Consumer while we upgrade it, seeing as it needs no updates to fulfill its work, what do we do? We stagger the updates:

In release 1, we update our message contracts by introducing thew new properties (new array of new complex type in this example). We add this property at the end of the contract, making sure we do not change the ordering of the properties in it. We flag the old property with the [Obsolete] attribute and release the contracts library as a new NuGet package.

After we've performed the updates in the contracts library, if we have time, we can choose to upgrade the service. This is done by letting it take a dependency on the new version of Contracts and implement the functionality required to fulfill the changes. Again, we won't remove any functionality in this phase - we will just add new functionality, even if it means that we have redundancies. With the new functionality in place, we'll release the service, knowing that the additive features we have added will not break the Monkey consumer, but that Tapir - if it upgrades its Banana Contracts dependency - is able to take advantage of the new goodies.

Tapir - being very much interested in this functionality - has actually been working in parallel, taking a dependency on the new version of Banana Contracts and is thereby ready to test as soon as Banana Service is released. Monkey, however, couldn't care less - it's busy with some important UX updates and will not upgrade this release.

The release date comes and ... nothing breaks! Tapir has its new functionality; Monkey its old and everyone is happy.

Knowing that new functionality was released during the previous release cycle (either through communication with the teams, or through a non-breaking upgrade of the Contracts library, seeing the Obsoletetion messages), the team behind the Monkey Consumer decides to upgrade their contract during Release 2. Worth noting, is that their organization has an understanding that any Consumer may lag behind at most one contract version at any one time, this decision wasn't hard (and that is part of this strategy).

Meanwhile, the team behind the Banana Service updates the Banana Contracts to v2.0.0, releases that and lets their Banana Service take a dependency on it, removing support for the old properties. When they finally release their software, Banana Service (using v2.0.0 of contracts), Monkey (using v1.1.0) and Tapir (using v1.1.0) will all be compatible with each other, whilst have being able to pursue their own business goals and controlling their own release schedule.

For a similar setup (and with code!), please see my versioning-strategies repository at GitHub.

Comments

Popular posts from this blog

Auto Mapper and Record Types - will they blend?

Unit testing your Azure functions - part 2: Queues and Blobs

Testing WCF services with user credentials and binary endpoints