Services should be decoupled from their clients as much as possible, especially when it comes to versioning and technologies. Any version of the client should be able to consume any version of the service and should do so without resorting to version numbers (such as those in assemblies), because those are .NET-specific. When a service and a client share a data contract, an important objective is to allow the service and client to evolve their versions of the data contract separately. To allow such decoupling, WCF needs to enable both backward and forward compatibility, without even sharing types or version information. There are three main versioning scenarios:
New members
Missing members
Round-tripping, in which a new data contract version is passed to and from a client or service with an older version, requiring both backward and forward compatibility
By default, data contracts are version-tolerant and will silently ignore incompatibilities.
The most common change made to a data contract is adding new
members on one side and sending the new contract to an old client or
service. When deserializing the type, DataContractSerializer
will simply ignore
the new members. As a result, both the service and the client can
accept data with new members that were not part of the original
contract. For example, suppose the service is built against this data
contract:
[DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; }
yet the client sends it this data contract instead:
[DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; [DataMember] public string Address; }
Note that adding new members and having them ignored in this way breaks the data contract schema compatibility, because a service (or a client) that is compatible with one schema is all of a sudden compatible with a new schema.
By default, WCF lets either party remove members from the data
contract. That is, you can serialize a type without certain members
and send it to another party that expects the missing members.
Although normally you probably won’t intentionally remove members, the
more likely scenario is when a client that is written against an old
definition of the data contract interacts with a service written
against a newer definition of that contract that expects new members.
When, on the receiving side, DataContractSerializer
does not find in the
message the information required to deserialize those members, it will
silently deserialize them to their default values; that is, null
for reference types and a zero
whitewash for value types. In effect, it will be as if the sending
party never initialized those members. This default policy enables a
service to accept data with missing members or return data with
missing members to the client. Example 3-13 demonstrates
this point.
Example 3-13. Missing members are initialized to their default values
/////////////////////////// Service Side ////////////////////////////// [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; [DataMember] public string Address; } [ServiceContract] interface IContactManager { [OperationContract] void AddContact(Contact contact); ... } class ContactManager : IContactManager { public void AddContact(Contact contact) { Trace.WriteLine("First name = " + contact.FirstName); Trace.WriteLine("Last name = " + contact.LastName); Trace.WriteLine("Address = " + (contact.Address ?? "Missing")); ... } ... } /////////////////////////// Client Side ////////////////////////////// [DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; } Contact contact = new Contact() { FirstName = "Juval", LastName = "Lowy" }; ContactManagerClient proxy = new ContactManagerClient(); proxy.AddContact(contact); proxy.Close();
The output of Example 3-13 will be:
First name = Juval Last name = Lowy Address = Missing
because the service received null
for the Address
data member and coalesced the trace
to Missing
. The problem with Example 3-13 is that you will
have to manually compensate this way at every place the service (or
any other service or client) uses this data contract.
When you do want to share your compensation logic
across all parties using the data contract, it’s better to use the
OnDeserializing
event to
initialize potentially missing data members based on some local
heuristic. If the message contains values for those members, they
will override your settings in the OnDeserializing
event. If it doesn’t, the
event handling method provides some nondefault values. Using the
technique shown here:
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
[DataMember]
public string Address;
[OnDeserializing]
void OnDeserializing(StreamingContext context)
{
Address = "Some default address";
}
}
the output of Example 3-13 will be:
First name = Juval Last name = Lowy Address = Some default address
Unlike ignoring new members, which for the most part is
benign, the default handling of missing members may very likely
cause the receiving side to fail further down the call chain,
because the missing members may be essential for correct operation.
This may have disastrous results. You can instruct WCF to avoid
invoking the operation and to fail the call if a data member is
missing by setting the IsRequired
property
of the DataMember
attribute to
true
:
[DataContract]
struct Contact
{
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
[DataMember(IsRequired = true
)]
public string Address;
}
The default value of IsRequired
is false
; that is, to ignore the missing
member. When, on the receiving side, DataContractSerializer
does not find the
information required to deserialize a member marked as required in
the message, it will abort the call, resulting in a NetDispatcherFaultException
on the sending
side. For instance, if the data contract on the service side in
Example 3-13 were to
mark the Address
member as
required, the call would not reach the service. The fact that a
particular member is required is published in the service metadata,
and when it is imported to the client, the generated proxy
definition will also have that member as required.
Both the client and the service can mark some or all of the data members in their data contracts as required, completely independently of each other. The more members that are marked as required, the safer the interaction with a service or a client will be, but at the expense of flexibility and versioning tolerance.
When a data contract that has a required new member is sent to
a receiving party that is not even aware of that member, such a call
is actually valid and will be allowed to go through. In other words,
if Version 2 (V2) of a data contract has a new member for which
IsRequired
is set to true
, you can send V2 to a party expecting
Version 1 (V1) that does not even have the member in the contract,
and the new member will simply be ignored. IsRequired
has an effect only when the
V2-aware party is missing the member. Assuming that V1 does not know
about a new member added by V2, Table 3-1 lists the
possible permutations of allowed or disallowed interactions as a
product of the versions involved and the value of the IsRequired
property.
An interesting situation relying on required members has to do
with serializable types. Since serializable types have no tolerance
for missing members by default, the resulting data contract will
have all data members as required when they are exported. For
example, this Contact
definition:
[Serializable] struct Contact { public string FirstName; public string LastName; }
will have the metadata representation:
[DataContract] struct Contact { [DataMember(IsRequired = true
)] public string FirstName {get;set;} [DataMember(IsRequired = true
)] public string LastName {get;set;} }
To set the same versioning tolerance regarding missing members
as the DataContract
attribute, apply the OptionalField
attribute on the optional member. For example, this Contact
definition:
[Serializable] struct Contact { public string FirstName; [OptionalField] public string LastName; }
will have the metadata representation:
[DataContract]
struct Contact
{
[DataMember(IsRequired = true)]
public string FirstName
{get;set;}
[DataMember]
public string LastName
{get;set;}
}
The versioning tolerance techniques discussed so far for ignoring new members and defaulting missing ones are suboptimal: they enable a point-to-point client-to-service call, but have no support for a wider-scope pass-through scenario. Consider the two interactions shown in Figure 3-4.
In the first interaction, a client that is built against a new data contract with new members passes that data contract to Service A, which does not know about the new members. Service A then passes the data to Service B, which is aware of the new data contract. However, the data passed from Service A to Service B does not contain the new members—they were silently dropped during deserialization from the client because they were not part of the data contract for Service A. A similar situation occurs in the second interaction, where a client that is aware of the new data contract with new members passes the data to Service C, which is aware only of the old contract that does not have the new members. The data Service C returns to the client will not have the new members.
This situation of new-old-new interaction is called a
versioning round-trip. WCF supports handling of
versioning round-trips by allowing a service (or client) with
knowledge of only the old contract to simply pass through the state of
the members defined in the new contract without dropping them. The
problem is how to enable services/clients that are not aware of the
new members to serialize and deserialize those unknown members without
their schemas, and where to store them between calls. WCF’s solution
is to have the data contract type implement the IExtensibleDataObject
interface, defined
as:
public interface IExtensibleDataObject { ExtensionDataObject ExtensionData {get;set;} }
IExtensibleDataObject
defines a single property of the type ExtensionDataObject
.
The exact definition of ExtensionDataObject
is irrelevant, since
developers never have to interact with it directly. ExtensionDataObject
has an internal linked
list of object
references and type
information, and that is where the unknown data members are stored. In
other words, if the data contract type supports IExtensibleDataObject
, when unrecognized new
members are available in the message, they are deserialized and stored
in that list. When the service (or client) calls out—passing the old
data contract type, which now includes the unknown data members inside
ExtensionDataObject
—the unknown
members are serialized out into the message in order. If the receiving
side knows about the new data contract, it will get a valid new data
contract without any missing members.
Example 3-14 demonstrates
implementing and relying on IExtensible
DataObject
. As you can see, the
implementation is straightforward: just add an Extension
DataObject
automatic
property with explicit interface implementation.
Example 3-14. Implementing IExtensibleDataObject
[DataContract]
class Contact : IExtensibleDataObject
{
ExtensionDataObject IExtensibleDataObject.ExtensionData
{get;set;}
[DataMember]
public string FirstName;
[DataMember]
public string LastName;
}
While implementing IExtensibleDataObject
enables
round-tripping, it has the downside of enabling a service that is
compatible with one data contract schema to interact successfully
with another service that expects another data contract schema. In
some esoteric cases, the service may decide to disallow
round-tripping and enforce its own version of the data contract on
downstream services. Using the ServiceBehavior
attribute (discussed at
length in Chapter 4), services can
instruct WCF to override the handling of unknown members by IExtensibleDataObject
and ignore them even
if the data contract supports IExtensibleDataObject
. The ServiceBehavior
attribute offers the Boolean property IgnoreExtensionDataObject
, defined
as:
[AttributeUsage(AttributeTargets.Class)] public sealed class ServiceBehaviorAttribute : Attribute,... { public bool IgnoreExtensionDataObject {get;set;} //More members }
The default value of IgnoreExtensionDataObject
is false
. Setting it to true
ensures that all unknown data members
across all data contracts used by the service will always be
ignored:
[ServiceBehavior(IgnoreExtensionDataObject = true
)]
class ContactManager : IContactManager
{...}
When you import a data contract using Visual Studio 2010, the
generated data contract type always supports IExtensibleDataObject
, even if the
original data contract did not. I believe that the best practice is
to always have your data contracts implement IExtensibleDataObject
and to avoid setting
IgnoreExtensionDataObject
to
true
. IExtensibleDataObject
decouples the
service from its downstream services, allowing them to evolve
separately.
13.59.177.14