Data Contract Versioning
As applications evolve, you may also have to change the data contracts the services use. This topic explains how to version data contracts. This topic describes the data contract versioning mechanisms. For a complete overview and prescriptive versioning guidance, see Best Practices: Data Contract Versioning.
Changes to a data contract can be breaking or nonbreaking. When a data contract is changed in a nonbreaking way, an application using the older version of the contract can communicate with an application using the newer version, and an application using the newer version of the contract can communicate with an application using the older version. On the other hand, a breaking change prevents communication in one or both directions.
Any changes to a type that do not affect how it is transmitted and received are nonbreaking. Such changes do not change the data contract, only the underlying type. For example, you can change the name of a field in a nonbreaking way if you then set the Name property of the DataMemberAttribute to the older version name. The following code shows version 1 of a data contract.
' Version 1 <DataContract()> _ Public Class Person <DataMember()> _ Private Phone As String End Class
The following code shows a nonbreaking change.
' Version 2. This is a non-breaking change because the data contract ' has not changed, even though the type has. <DataContract()> _ Public Class Person <DataMember(Name := "Phone")> _ Private Telephone As String End Class
Some changes do modify the transmitted data, but may or may not be breaking. The following changes are always breaking:
Renaming a data member.
Changing the data contract of a data member. For example, changing the type of data member from an integer to a string, or from a type with a data contract named "Customer" to a type with a data contract named "Person".
The following changes are also possible.
In most cases, adding or removing a data member is not a breaking change, unless you require strict schema validity (new instances validating against the old schema).
When a type with an extra field is deserialized into a type with a missing field, the extra information is ignored. (It may also be stored for round-tripping purposes; for more information, see Forward-Compatible Data Contracts).
When a type with a missing field is deserialized into a type with an extra field, the extra field is left at its default value, usually zero or
null. (The default value may be changed; for more information, see Version-Tolerant Serialization Callbacks.)
For example, you can use the
CarV1 class on a client and the
CarV2 class on a service, or you can use the
CarV1 class on a service and the
CarV2 class on a client.
' Version 1 of a data contract, on machine V1. <DataContract(Name := "Car")> _ Public Class CarV1 <DataMember()> _ Private Model As String End Class ' Version 2 of the same data contract, on machine V2. <DataContract(Name := "Car")> _ Public Class CarV2 <DataMember()> _ Private Model As String <DataMember()> _ Private HorsePower As Integer End Class
The version 2 endpoint can successfully send data to the version 1 endpoint. Serializing version 2 of the
Car data contract yields XML similar to the following.
<Car> <Model>Porsche</Model> <HorsePower>300</HorsePower> </Car>
The deserialization engine on V1 does not find a matching data member for the
HorsePower field, and discards that data.
Also, the version 1 endpoint can send data to the version 2 endpoint. Serializing version 1 of the
Car data contract yields XML similar to the following.
<Car> <Model>Porsche</Model> </Car>
The version 2 deserializer does not know what to set the
HorsePower field to, because there is no matching data in the incoming XML. Instead, the field is set to the default value of 0.
A data member may be marked as being required by setting the IsRequired property of the DataMemberAttribute to
true. If required data is missing while deserializing, an exception is thrown instead of setting the data member to its default value.
Adding a required data member is a breaking change. That is, the newer type can still be sent to endpoints with the older type, but not the other way around. Removing a data member that was marked as required in any prior version is also a breaking change.
Changing the IsRequired property value from
false is not breaking, but changing it from
true may be breaking if any prior versions of the type do not have the data member in question.
Although the IsRequired property is set to
It is possible (although not recommended) to set the
EmitDefaultValue property on the DataMemberAttribute attribute to
false, as described in Data Member Default Values. If this setting is
false, the data member will not be emitted if it is set to its default value (usually null or zero). This is not compatible with required data members in different versions in two ways:
A data contract with a data member that is required in one version cannot receive default (null or zero) data from a different version in which the data member has
A required data member that has
falsecannot be used to serialize its default (null or zero) value, but can receive such a value on deserialization. This creates a round-tripping problem (data can be read in but the same data cannot then be written out). Therefore, if
falsein one version, the same combination should apply to all other versions such that no version of the data contract would be able to produce a value that does not result in a round trip.
For an explanation of what schema is produced for data contract types, see Data Contract Schema Reference.
The schema WCF produces for data contract types makes no provisions for versioning. That is, the schema exported from a certain version of a type contains only those data members present in that version. Implementing the IExtensibleDataObject interface does not change the schema for a type.
Data members are exported to the schema as optional elements by default. That is, the
minOccurs (XML attribute) value is set to 0. Required data members are exported with
minOccurs set to 1.
Many of the changes considered to be nonbreaking are actually breaking if strict adherence to the schema is required. In the preceding example, a
CarV1 instance with just the
Model element would validate against the
CarV2 schema (which has both
Horsepower, but both are optional). However, the reverse is not true: a
CarV2 instance would fail validation against the
Round-tripping also entails some additional considerations. For more information, see the "Schema Considerations" section in Forward-Compatible Data Contracts.
Implementing the IExtensibleDataObject interface is a nonbreaking change. However, round-tripping support does not exist for versions of the type prior to the version in which IExtensibleDataObject was implemented. For more information, seeForward-Compatible Data Contracts.
Adding or removing an enumeration member is a breaking change. Changing the name of an enumeration member is breaking, unless its contract name is kept the same as in the old version by using the
EnumMemberAtttribute attribute. For more information, seeEnumeration Types in Data Contracts.
Most collection changes are nonbreaking because most collection types are interchangeable with each other in the data contract model. However, making a noncustomized collection customized or vice versa is a breaking change. Also, changing the collection's customization settings is a breaking change; that is, changing its data contract name and namespace, repeating element name, key element name, and value element name. For more information about collection customization, see Collection Types in Data Contracts.
Naturally, changing the data contract of contents of a collection (for example, changing from a list of integers to a list of strings) is a breaking change.
Version-Tolerant Serialization Callbacks
Best Practices: Data Contract Versioning
Using Data Contracts
Data Contract Equivalence
Forward-Compatible Data Contracts