devxlogo

Best Practices for Handling Change in Your WCF Applications

Best Practices for Handling Change in Your WCF Applications

hange is the one constant you can depend on: Requirements change, environments change, and processes change. Together, these factors ensure that your WCF services will change as well. Fortunately, you can make some basic design decisions at the outset that will make these changes much less disruptive to your service consumers and, in the end, to yourself.

This article explores not only the upfront decisions you can make to minimize changes, but also the strategies you can employ to deal with any big changes that your service consumers may not have foreseen, but that you know are coming.

Defining Change

Before diving into how to deal with change, it makes sense to explain what change means in a WCF-based service. The following actions (listed by type of contract) constitute changes:

  • Data contracts
    • Adding a data member
    • Removing a data member
    • Renaming a data member
    • Changing the type of a data member
  • Service contracts
    • Adding an operation
    • Removing an operation
    • Renaming the service contract
  • Operation contracts
    • Renaming the operation
    • Changing the signature of an operation

These changes might be the result of new business requirements, hardware consolidation, business mergers, new regulations, or any number of other external factors. The bottom line is that when something outside the developer’s control changes, the software must adjust. Dealing with change in the WCF world is a constant case of good news/bad news. You can handle some scenarios quite easily, while others will lead you to give the dreaded “yes, but?” response.

Versioning and Change in WCF

In the .NET world, one of the first considerations that come to mind when dealing with change is versioning. You can version assemblies to allow for unforeseen or breaking changes in later revisions of a component. That way, the affected clients can continue to utilize an older version of an assembly and you can avoid the headaches associated with a breaking change.

The logical question then is ‘does WCF support versioning?’ The answer is that dreaded “yes, but?” When you create a data contract in WCF, the contract generates an XML schema. Consumers reference this schema and use it to generate a proxy class. The data is not, strictly speaking, validated against this schema going forward. As you will see, this can sometimes cause unexpected and frustrating behaviors for service consumers.

Before diving into specifics, familiarize yourself with the following sample service. It provides the basis for the discussion in the remainder of the article.

namespace SampleService{    [ServiceContract]    public interface IPersonService    {        [OperationContract]        Person GetPerson(int personId);        [OperationContract]        void UpdatePerson(Person p);    }    public class Person    {        private string _firstName = string.Empty;        private string _lastName = string.Empty;        [DataMember]        public string FirstName        {            get { return _firstName; }            set { _firstName = value; }        }        [DataMember]        public string LastName        {            get { return _lastName; }            set { _lastName = value; }        }    }}

Data Contract Changes

The Person DataContract defines two attributes: FirstName and LastName. If a client were to reference this service and you were to subsequently change LastName to SurName, the client would not actually break but the LastName attribute on the client’s proxy class would appear to be empty. This is because when the client deserializes the message into the Person class, it will no longer find any element named LastName to place into the attribute.

This simple change won’t cause the client to experience an error; it will cause something even worse: an unexpected behavior. The error would be easy to track down, but tracking down the change in behavior is much more difficult.

This simple example demonstrates why it is critical to consider any service changes and their downstream effects. Unless you are personally aware of every client application utilizing your web service, changes can be disastrous. As a developer, you should do everything in your power to shield your clients from changes.

Initially, you can first apply a few best practices that will help isolate clients from internal changes. An updated version of the data contract might look like this:

[DataContract(Namespace="http://types.mycompany.com/2009/05/25", Name="PersonContract")]public class Person : IExtensibleDataObject{    private string _firstName = string.Empty;    private string _lastName = string.Empty;    private ExtensionDataObject _extensionData;    [DataMember(Name="FirstName")]    public string FirstName    {        get { return _firstName; }        set { _firstName = value; }    }    [DataMember(Name="LastName")]    public string LastName    {        get { return _lastName; }        set { _lastName = value; }    }    public ExtensionDataObject ExtensionData    {        get { return _extensionData; }        set { _extensionData = value; }    }}

The addition of the Namespace, Name, and Order parameters (you will see Order in an upcoming code example) on the DataContract and DataMember attributes controls the DataContractSerializer’s behavior. This addition generates the client proxy when a reference to the service is added. The Name parameters cause the serializer to use the indicated value as opposed to the name of the actual public member or property. This approach allows the internal implementation to change without affecting the client. For example, consider the following change:

[DataMember(Name="LastName")]    public string SurName    {        get { return _lastName; }        set { _lastName = value; }    }

The change of the property name from “LastName” to “SurName” will not break existing clients because the Name parameter that clients use is still “LastName.” Only the internal implementation has changed.

The second noticeable change is the addition of the IExtensibleDataObject interface. Implementing this interface allows a client to retain data that is not explicitly defined in the contract. This may not seem immediately useful, but in cases where the client is expected to perform processing on the sample Person object and return it, the client can retain new data items. For example, updating the PersonContract with the following new member will not force existing clients to be updated:

[DataMember(Name = "MiddleName", Order = 3)]public string SurName{    get { return _middleName; }    set { _middleName = value; }}

This member will, in fact, allow existing clients to retain a value placed in “MiddleName” by the service over the round trip. Implementing the IExtensibleDataObject is a useful way to future-proof your data contracts. As a best practice, you should use it on all data contracts.

Keep in mind that clients actually have the option of validating messages against an external schema. (For a thorough discussion of adding message schema validation to a WCF application, read this article.) As a result, you have two scenarios to consider when dealing with changes to data contracts: those with schema validation and those without schema validation.

When clients add schema validation, adding, changing, or subtracting any items in a data contract will cause that validation to fail. So, in cases where strict schema validation is being used, contracts simply should not be changed. Instead, you should create an entirely new contract and use a different namespace in the contract to indicate the new version.

For example, from an implementation perspective, you would need two separate service endpoints to make both these versions available:

Original Version: 
[DataContract(Namespace="http://schemas.mycompany.com/2009/05/25")]New Version:
[DataContract(Namespace="http://schemas.mycompany.com/2009/06/18")]

Fortunately, strict schema validation is not the default behavior. This means you can actually add and remove data members without breaking the client. However, because of the unexpected behavior previously discussed, removing a data member is not a good idea. On the other hand, adding a data member is easy to do, and consumers will ignore extra members that they are not aware of.

A key practice is to use the Order parameter of the DataMember attribute (as previously discussed). Using this parameter tells the serializer in what order each member should appear in the XML. An unintended change in the order can cause the XML to no longer conform to the original schema. Simply using the Order parameter from the beginning can avoid this problem. If you do not use the Order parameter, the serializer follows this order:

  1. Members from base types
  2. Members without an Order parameter (alphabetically)
  3. Members with an order parameter (in order by value)

The final scenario to consider with data contracts is changing the type of a data member. In this case, the best approach is to create a new version of the data contract along with a new service contract, implementation, and endpoint.

Service Contract Changes

Once again, all service contracts should follow the best practice of using both the Name and Namespace parameters on the ServiceContract attribute. An updated version of the Person service contract might look like this:

[ServiceContract(Name="PersonService", Namespace="http://services.mycompany.com/2009/05/25"]public interface IPersonService

As with the data contract, using the Name isolated the service consumer from the actual interface name, allowing the internal implementation to change as needed. The addition of the Namespace allows you to version the contract at some point in the future. Remember that new versions also require new endpoints.

You can add operations to service contracts without breaking existing consumers. Consumers will simply ignore the new operations. Removing operations, on the other hand, will break existing consumers. As with all breaking changes, the removal of an operation requires a new version along with a new endpoint.

Operation Contract Changes

As with the service and data contracts, you should use the Name parameter with the OperationContract attribute:

[OperationContract(Name="GetPerson"]Person GetPerson(int personId);

The consumer is, once again, isolated from changes in the internal implementation.

Finally, the last change to consider is in the signature of an operation contract. This is a breaking change for which you have two solutions: create a new version or add a new operation to the service contract.

Honor Your Commitments

Change is inevitable, but with a little planning and a few key principles, you can minimize the impact changes have on your WCF services. Always remember that when you release a service, you make a commitment to the service consumers to honor the contract. Making changes to an existing contract is bad form.

To that end, keep some best practices in mind:

  1. Always use the Name and Namespace parameters on all contracts.
  2. Always use the Order parameter on data members.
  3. Implement the IExtensibleDataObject on all data contracts.
  4. Use namespaces for contract versioning.
  5. Always remember that new versions require new endpoints.
  6. When using strict schema validation, do not change contracts. Create new versions.
  7. When removing an operation from a service contract, create a new version.
  8. When changing the signature of an operation, create a new version.

With these practices in mind, you can make change just a little easier to dealing with for yourself and your service consumers.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist