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 allowing 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, where 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, the service may be built against
this data contract:
[DataContract] struct Contact { [DataMember] public string FirstName; [DataMember] public string LastName; }
yet the client may send 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-7 demonstrates this point.
Example 3-7. 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-7 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-7 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-7 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-7 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.
Table 3-1. Versioning tolerance with required members
IsRequired |
V1 to V2 |
V2 to V1 |
---|---|---|
|
Yes |
Yes |
|
No |
Yes |
An interesting situation relying on required members has to do with serializable
types. Since serializable types have no tolerance toward missing members by default,
when they are exported the resulting data contract will have all data members as
required. For example, this Contact
definition:
[Serializable] struct Contact { public string FirstName; public string LastName; }
will have the metadata representation of:
[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 of:
[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-8 demonstrates implementing and
relying on IExtensibleDataObject
. As you can see, the
implementation is straightforward: just add an ExtensionDataObject
automatic property with explicit interface
implementation.
Example 3-8. 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 the next chapter), 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 2008, 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.
There is no need to implement IExtensibleDataObject
when dealing with known types, because the subclass
is always deserialized without a loss.
3.145.78.136