Overview
Services exist in order to serve external clients. They do so by exposing a
wide range of external interfaces which are later used by the clients to
interact with the service.
After initial deployment, and potentially several times during their lifetime,
services may need to be changed for a variety of reasons, such as changing
business needs, re-factorization of code, or to address other issues.
Each change introduces a new version of the service. Each new service version
potentially introduces changes to the external interfaces exposed by the
service.
Our goal, as developers, is to be able to freely change our service internals
(and sometimes, when needed, external interfaces), and on the same time, allow
existing and new clients to keep using the service.
In order to do so, we need to form a set of versioning guidelines.
This article will cover basic concepts related to versioning issues and will
provide a set of guidelines which aim to provide a simple solution for the
versioning problem domain.
All the information exists freely on the Internet. We didn’t invent anything
new. We just gathered most of the information in one simple blog post, for your
convenience.
A lot of the guidelines below were taken from an excellent set of MSDN
articles:
http://msdn.microsoft.com/en-us/library/ms731060.aspx and http://msdn.microsoft.com/en-us/library/ms733832.aspx.
These articles are very good reading material.
Reduce the Problem Domain
Services interact with external services in many different ways. They can use
WCF to send and receive information. They can connect to external databases
directly (by using ADO.NET, for example) and many more.
Instead of forming a set of guidelines for each protocol, the simplest way is
to reduce the problem domain to one protocol and use only one set of guidelines
for this protocol.
For example, instead of connecting to a database directly, it is possible to
create an abstraction layer on top of the database, which will expose WCF
interface to the clients. This layer will be upgraded together with the
database, creating a coupling between this layer and the database. The exposed
WCF interface will follow the versioning guidelines that we will mention later,
thus keeping all the clients compatible with the database service, even when it
is being upgraded.
To sum it up, if you like the WCF versioning guidelines that we’ll mention
later on, use WCF interfaces whenever possible to expose your service
functionality.
Versioning Terminology
Strict Versioning – In many scenarios when a change is required to an external
interface, the service developer does not have control over the clients and
therefore cannot make assumptions about how they would react to changes in the
message XML or schema. The recommended approach in such scenarios is to treat
existing data contracts as immutable and create new data contracts (with unique
XML qualified names) instead.
Lax Versioning – In many other scenarios, the service developer can make the
assumption that adding a new, optional member to the data contract will not break
existing clients. This requires the service developer to investigate whether
existing clients are not performing schema validation and that they ignore
unknown data members. In these scenarios, it is possible to take advantage of
data contract features for adding new members in a non-breaking way. WCF
support Lax versioning: that is, they do not throw exceptions for new unknown
data members in received data, unless requested otherwise.
When a connection is made between a client and a server of a different version,
and a strict versioning is used, the client won’t be able to connect to a
service of a different version. When lax versioning is used, the client will be
able to connect and it is up to the developer to handle backward and forward
compatibility.
Client\Server Paradigm
In a client\server world, a client sends requests to a server, which, in turn,
responds back to the client.
When versioning is introduced into the system, we may encounter the following
scenarios:
Client and server use the same version.
Client is older than the server.
Client is newer than the server.
Each scenario should be handled separately, but there is one rule which applies
to all: Newer element knows more than older element, therefore, newer elements
should be the more proactive elements in the versioning game.
This means the following:
Client and server use the same version – No problem here. Communication will
work as expected.
Client is older than the server – The server should support both older and
current clients. When Lax Versioning is used, the server is allowed to add new
data members and new operations (according to Lax guidelines which will be
described later on). It must expose the same WCF service, on the same contract
with the same name and namespace. When Strict Versioning is used, the server
must expose (at least) two WCF services (and WCF contracts). The first is the
new WCF service, on the new contract with a new namespace and name, and the
rest are all the supported old WCF services with their original contacts,
namespaces and names.
Client is newer than the server – The client must avoid using unsupported
operations on an older server. When Lax versioning is used, the client is not
allowed to call new operations (which don’t exist on the server side). When
Strict versioning is used, the client must use a WCF endpoint which exposes a
contract matching the one used by the server. In both approaches, the client
must know the version of the server. This is performed by some kind a version
discovery mechanism (more on this below).
Note – If an element acts both as a client and as a server, it is very
reasonable that it would suffer from both issues above, during the upgrade
period. This means that it will probably have a local client newer than a
remote server and a local server newer than a remote client. In these
situations, each direction should be handled separately.
Version Discovery Basics
When a client is newer than the server, as we already know, it must protect
itself when communicating with an older server.
For example, if Lax versioning is used, the client should be responsible not to
call new operations which are not supported by the server.
When Strict versioning is used, it should find a matching contract, support by
the server, to communicate on.
In order to do that, the client must discover the server’s version. This is
called Version Discovery.
There are several ways to discover the server’s version:
Get Version API – The server should expose its version somehow. The client
should use this API to discover the server’s version before attempting to
communicate with the server.
Fallback – The client tries to communicate with the server with the newest
contract version known to the client, and falls back to older versions when the
communication fails until it finds a contract version match (of course, this
can only work with Strict versioning).
Configuration – The server version is stored somewhere in local configuration
and the client can read it before attempting to connect to the server.
Versioning Guidelines
So far we had a long introduction to the versioning world. It’s time to
actually discuss some actual versioning guidelines.
First we will try to understand when to use Lax versioning and when to use
Strict versioning. Later on, we will provide a set of guidelines for each
versioning schema.
When building a complex system, involving multiple machines working together to
expose one big external service, we can distinguish between two types of
communication flows, inner communication an external communication.
Inner Communication – This covers all communication flows between the system’s
machines. In such systems, usually, the developers have control on all
components involved in the communication flows. Therefore, the natural
versioning schema should be Lax versioning. As a reminder, Lax versioning can
be used when we know something about the involved components and can enforce several
assumptions on them. Of course, if we can’t make these assumptions, or if we
require huge contract changes which can’t be covered by the Lax versioning
guidelines (which will be covered later on), we can always fallback to Strict
versioning.
External Communication – This covers all communication between internal
endpoints (controlled by the system developers) to external endpoints (not
controlled by the system developers, naturally). Since we don’t control one
side of the conversation, Strict versioning is the only way to go. This means
that whenever a new version is available, we create a new contract to represent
it, while keeping the old contract intact. We must publish both the new
contract and the old contract (or contracts, if we want to support several
versions back) simultaneously. We might also consider providing some assistance
in terms of providing some version discovery mechanisms. If clients are newer
than the server, they will have to use one of the version discovery mechanisms
to survive.
Lax Versioning Guidelines
The guidelines are divided to service contract guidelines and data contract
guidelines.
Lax Service Contract:
Namespace and Name – DO NOT change namespace or name between
versions. Do not add namespace or name, if the contract didn’t have namespace
or name in the previous version (unless you want to add the default namespace
and name)
Operations Addition – Adding service operations exposed by the service can be
considered as a non-breaking change because existing (older) clients need not
be concerned about those new operations. Newer clients must use version
discovery methods before contacting an older server and avoid calling new
operations. If version discovery is impossible, adding operations is considered
as a breaking change (strict).
Operations Removal – Removing operations is considered to be a breaking change
and therefore, NOT ALLOWED. To make such a change, strict versioning should be
used (define a new service contract and expose it on a new endpoint). It is still
possible to remove operations, but this can be performed only after the
versioning difference between a client and a server exceeds the maximum allowed
by the system (for example, we support only one version back and we now upgrade
to version N+2).
Operation Parameters Types – Changing parameter types or return types generally
is considered to be a breaking change unless the new type implements the same
data contract implemented by the old type. Other type changes are NOT ALLOWED.
To make such changes, add a new operation to the service contract or use strict
versioning.
Operation Parameters Addition/Removal – Adding or removing an operation
parameter is a breaking change, therefore, it is NOT ALLOWED. To make such a
change, add a new operation to the service contract or use strict versioning.
Operation Parameters Aggregation – It is recommended to use one aggregated data
contract as an operation parameter, rather than separate multiple parameters.
By using a data contract as aggregated parameter, while keeping the Lax
guidelines for data contracts (see below), it is easier to keep an operation
backward compatible. It is possible to change internal parameters without being
limited to the above restrictions.
Fault Contracts – The list of faults described in a service’s contract is not
considered exhaustive. At any time, an operation may return faults that are not
described in its contract. Therefore changing the set of faults described in
the contract is not considered a breaking change. For example, adding a new
fault to the contract using the FaultContractAttribute or removing an existing
fault from the contract is allowed.
Lax Data Contract:
Namespace and Name – DO NOT change namespace or name between versions. Do not
add namespace or name, if the contract didn’t have namespace or name in the
previous version (unless you want to add the default namespace and name)
Data Contract Names - In later versions, DO NOT change the data contract name
or namespace. If changing the name or namespace of the type underlying the data
contract, be sure to preserve the data contract name and namespace by using the
appropriate mechanisms, such as the Name property of the DataContractAttribute.
Data Members Names – In later versions, DO NOT change the names of any data
members. If changing the name of the field, property, or event underlying the
data member, use the Name property of the DataMemberAttribute to preserve the
existing data member name.
Data Members Types – In later versions, DO NOT change the type of any field,
property, or event underlying a data member such that the resulting data
contract for that data member changes. Keep in mind that interface types are
equivalent to Object for the purposes of determining the expected data
contract.
Data Members Addition – In later versions, new data members can be added. They
should always follow these rules:
a. The IsRequired property should always be left at its default value of false.
b. If a default value of null or zero for the member is unacceptable, a
callback method should be provided using the OnDeserializingAttribute to
provide a reasonable default in case the member is not present in the incoming
stream.
c. The Order property on the DataMemberAttribute should be used to make sure
that all of the newly added data members appear after the existing data
members. The recommended way of doing this is as follows: None of the data
members in the first version of the data contract should have their Order
property set. All of the data members added in version 2 of the data contract
should have their Order property set to 2. All of the data members added in
version 3 of the data contract should have their Order set to 3, and so on. It
is permissible to have more than one data member set to the same Order number.
Data Members Order – In later versions, DO NOT change the order of the existing
data members by adjusting the Order property of the DataMemberAttribute
attribute.
Data Members Removal – DO NOT remove data members in later versions, even if
the IsRequired property was left at its default property of false in prior
versions.
Data Members IsRequired –
a. DO NOT change the IsRequired property on any existing data members from
version to version.
b. For required data members (where IsRequired is true), DO NOT change the
EmitDefaultValue property from version to version.
Branching – DO NOT attempt to create branched versioning hierarchies. That is,
there should always be a path in at least one direction from any version to any
other version using only the changes permitted by these guidelines. For
example, if version 1 of a Person data contract contains only the Name data
member, you should not create version 2a of the contract adding only the Age
member and version 2b adding only the Address member. Going from 2a to 2b would
involve removing Age and adding Address; Going in the other direction would
entail removing Address and adding Age. Removing members is not permitted by
these guidelines.
Known Types – You should generally not create new subtypes of existing data
contract types in a new version of your application. Likewise, you should not
create new data contracts that are used in place of data members declared as
Object or as interface types. For example, in version 1 of your application,
you may have the LibraryItem data contract type with the Book and Newspaper
data contract subtypes. LibraryItem would then have a known types list that
contains Book and Newspaper. Suppose you now add a Magazine type in version 2
which is a subtype of LibraryItem. If you send a Magazine instance from version
2 to version 1, the Magazine data contract is not found in the list of known
types and an exception is thrown. Creating these new classes is allowed only
when you know that you can add the new types to the known types list of all
instances of your old application or if newer instances use version discovery
and will not send new types to an old server.
Enumerations – DO NOT add or remove enumeration members between versions. You
should also not rename enumeration members; unless you use the Name property on
the EnumMemberAttribute attribute to keep their names in the data contract
model the same.
Inheritance –
DO NOT attempt to version data contracts by type
inheritance. To create later versions, either change the data contract on an
existing type (Lax) or create a new unrelated type (Strict).
The use of inheritance together with data contracts is allowed, provided that
inheritance is not used as a versioning mechanism and that certain rules are
followed. If a type derives from a certain base type, do not make it derive
from a different base type in a future version (unless it has the same data
contract). There is one exception to this: you can insert a type into the
hierarchy between a data contract type and its base type, but only if it does
not contain data members with the same names as other members in any possible
versions of the other types in the hierarchy. In general, using data members
with the same names at different levels of the same inheritance hierarchy can
lead to serious versioning problems and therefore, it is prohibited.
Collections – Collections are interchangeable in the data contract model. This
allows for a great degree of flexibility. However, make sure that you do not
inadvertently change a collection type in a non-interchangeable way from
version to version. For example, do not change from a non-customized collection
(that is, without the CollectionDataContractAttribute attribute) to a
customized one or a customized collection to a non-customized one. Also, do not
change the properties on the CollectionDataContractAttribute from version to
version. The only allowed change is adding a Name or Namespace property if the
underlying collection type’s name or namespace has changed and you need to make
its data contract name and namespace the same as in a previous version.
Round Tripping – In some scenarios, there is a need to “round-trip” unknown
data that comes from members added in a new version. For example, a
“versionNew” service sends data with some newly added members to a “versionOld”
client. The client ignores the newly added members when processing the message,
but it resends that same data, including the newly added members, back to the
versionNew service. The typical scenario for this is data updates where data is
retrieved from the service, changed, and returned. If the data contract is
expected to be used in a round tripping scenario, then starting with the first
version of a data contract, always implement IExtensibleDataObject to enable
round-tripping. If you have released one or more versions of a type without
implementing this interface, it is usually recommended to implement it in the
next version of the type.
Strict Versioning Guidelines
Strict versioning guidelines are also divided into service
contract and data contract guidelines, although they are very similar.
Strict Service Contract:
Namespace and Name – At least one of the namespace or name MUST be changed in
later version, in order to break the contract compatibility. If the contract
didn’t have namespace or name in previous version, namespace and name must be
added to the contract (and must have different values than the default values)
Cascading Break – If a service contract exposes a data contract directly or via
operations parameters or return value, and this data contract compatibility is
broken (namespace or name replaced), then the service is also broken and MUST
be treated as such, thus, a new contract should be created.
Immutability – When breaking a service contract, a new service contract MUST be
created with a unique Namespace and Name, while the old contract MUST be kept
intact and treated as immutable.
Strict Data Contract:
Namespace and Name – At least one of the namespace or name MUST be changed in
later version, in order to break the contract compatibility. If the contract
didn’t have namespace or name in previous version, namespace and name MUST be
added to the contract (and must have different values than the default values)
Immutability – When breaking a data contract, a new data
contract MUST be created with a unique Namespace and Name, while the old
contract MUST be kept intact and treated as immutable.
Cascading Break – If a data contract is contained in a
different data contract or exposed by a service contract directly or
indirectly, and this data contract compatibility is broken (namespace or name
replaced), then all contracts containing or exposing this data contract MUST be
broken as well