In message-based systems, the messages are part of the contract, which defines how services communicate with each other.
Evolving contracts over time is not an easy task and the appropriate strategy should be defined individually for each system. Generally the problem can't be resolved at the infrastructure level; therefore NServiceBus users must analyze their individual systems, consider how they are expected to evolve, and define the strategy which will make the most sense in their particular circumstances.
This article presents basic guidelines for choosing contracts evolution strategy, avoiding common mistakes, and ensuring that contracts will be easy to evolve over time.
Ensure that messages can be evolved by following general messages design guidelines.
Messages define the data contract between two endpoints.
It's recommended to use a dedicated assembly for message definitions. By keeping messages in a separate assembly, the amount of information and dependencies shared between services is minimized. Message definitions can be divided between multiple assemblies, which can be useful in more complex systems, for example, to narrow down the number of contracts exposed to different services.
It's also possible to share messages as C# source files without packaging them into an assembly. One advantage of this approach is that messages don't need to be compiled against specific NServiceBus versions, so assembly redirects are not necessary.
When a message is extended with additional properties, it's necessary to carefully examine what will be the default values for those properties, especially if endpoints running in other versions don't recognize them. In particular, it's important to consider how clients might interpret the default value and provide appropriate guidelines for them.
There are a number of approaches to handling breaking changes in messages. One of the simplest is adding a version to the type name and creating handlers for the new contract. The handlers for the old contract might still be running, to handle in-flight messages and messages generated by old endpoints.
When there are significant changes in a message type, such as adding or removing properties, or changing the property type, the upgrade process should consist of the following steps:
- Update the contract to the new version.
- Update senders to use the new contract version. Ensure changes are visible for receivers, for example, by decorating the existing property with the
Obsoleteattribute when removing or renaming properties.
- Update receivers to handle the new contract version. Make sure the new properties are handled correctly, e.g. instead of relying on .NET to set the default value for
int Age = 1, it's better to use nullable types and represent missing values as
- When all senders and receivers are updated and in-flight messages in the old format have been handled, obsolete the properties and throw an error, or simply remove them.
Another approach for handling breaking changes is to modify serialization formats. The step-by-step guidance is provided in the transition serialization formats formats.