2

Let's say we use message contracts based on interfaces, as recommended for MassTransit for example.

First how are those interfaces shared across all the applications? Let's say we provide them in a nuget package. (Is that the way to go?)

So second, how do we now make sure all the applications use the same version?

Should we use new interfaces every time (e.g. messageV1, messageV2) to be backwards compatible? That would require us to send multiple messages at once instead of 1...

Or should we have an upgrade window, where all applications are updated at the same time?


Please check out both the answers and the comments, if you are looking in the same.
Really got some quality feedback here :D

SwissCoder
  • 2,514
  • 4
  • 28
  • 40
  • 1
    Some great answers already, so I won't add another one. The documentation does give good suggestions on using nullable properties and ensure the consumer can adapt if new values are missing. Also, contract ownership is very important. Don't share contracts. Event publishers own their event contracts, and service consumers own their command contracts. Avoid the "don't repeat yourself" trap, and keep those contracts separate. – Chris Patterson Oct 15 '19 at 12:56
  • thanks for your Inputs Chris! Let's assume we have 2 separate backends for mobile apps, and a website. they should all be allowed to send an order. Wouldn't we reuse the same "order"-message in such a case? The way to go would be for all 3 systems to send different messages containing identical content? And the processing end then treats the 3 different messages the same way, ok. But all the aplications would need the contracts defined in one or several nuget package(s). How can some applications own contracts, if they are not exposing them anywhere to the consumers? – SwissCoder Oct 15 '19 at 14:00
  • 1
    Each back end would be essentially providing their own "orchestration" to the mobile app, but both of the back ends would send the same SubmitOrder contract to the order service. The order service would own that contract. – Chris Patterson Oct 15 '19 at 20:10

2 Answers2

3

MassTransit doesn't explicitly support any kind of versioning, so you're left with the freedom to choose to do what you think is best. The assumptions you've made in your question are more or less exactly the way I do things:

  • Contracts are shared as a nuget package across subsystems
  • New interfaces are created when changes need to be made, interfaces are only ever extended with nullable / backwards compatible changes
  • If necessary, multiple messages are published/sent to preserve backwards compatibility
  • When no longer needed, older versions can be obsoleted/removed

It can seem like a lot of work, but if you design things to work that way from the start it's not so bad, and it really pays off.

nizmow
  • 579
  • 2
  • 10
  • Thanks a lot Neil! I am a bit surprised that things actually are that way. In an agile enviroment where you constantly extend the interfaces, this really seems like a lot of work. :/ I guess it could lead to inflated interfaces, that specify more properties than currently implemented, due to the use of fields in the "future". – SwissCoder Oct 15 '19 at 10:56
  • 1
    can you change "interfaces are never modified" to "interfaces can be extended with nullable properties"? So I could accept your answer as the solution, since it's more compact, describing the whole approach. (Although Alexey's details are sure super helpful as well, to understand the masstransit versioning.) – SwissCoder Oct 15 '19 at 12:20
3

Message contracts aren't different from any other kind of API contract and you treat them accordingly. Consider any public API as something that went to the world and you cannot control everyone who uses the API.

There're quite a few good guidelines about contract versioning and there's nothing specific to MassTransit there.

MassTransit, on the other hand, provides a few suggestions on how to deal with message versions in the documentation. In particular, we suggest not creating a new version if you follow the Weak Schema approach and add properties that can be empty and aren't important for the consumer. In addition, you can use interface composition, like this example from the docs:

class RemoteImageCachedEvent :
    RemoteImageCached,
    RemoteImageCachedV2
{
    Guid EventId { get; set; }
    DateTime Timestamp { get; set; }
    Guid InitiatingCommandId { get; set; }
    Uri ImageSource { get; set; }
    string LocalCacheKey { get; set; }
    Uri LocalImageAddress { get; set; }
}

so when you publish an instance of RemoteImageCachedEvent, it will be delivered to message exchanges for both interfaces.

If you completely change your API surface, would this be messages, or Grpc, or REST, you need to consider backwards compatibility and keep consuming both old and new messages for a while. Give people some time to change from one version to another and kill the old version if you need to.

Alexey Zimarev
  • 17,944
  • 2
  • 55
  • 83
  • awesome! (not) sure how I missed that :) I didn't RTFM completely. Adding nullable properties sure does take away some of the pain. In a normal API environment I would not have "globally" valid Messages, but only messages that are valid per API. With the BUS we now have scenarios that different applications publish the same messages.. so we have one additional dimension to take care of :) – SwissCoder Oct 15 '19 at 12:16