While SOA promotes loose coupling at the transport layer and also facilitates platform independence between consumers and the services they interact with, we are still coupled to the interface as defined by the service’s contract. We’ve learned a lot about how to create loosely coupled systems over the last few decades, but how can we apply those lessons to services? This article expands upon Martin Fowler’s Service Layer Pattern. It presents patterns that allow you to define descriptive, maintainable, yet extensible contracts specifically with Microsoft’s WCF technology.
Figure 1 illustrates the significant components I will discuss. The primary players include Data Transfer Objects (a.k.a. DTOs), Service Messages, and the Service Contract. The DTOs and Service Messages in particular serve as reusable types that allow you to compose complex operations in a building block style. They can also be independently versioned and deployed.
Figure 1
Data Transfer Objects (a.k.a. DTOs) were first made popular in the Java programming language. They may be thought of as being reusable complex data types that typically represent business entities. Examples of DTOs used for a commercial airline, the sample scenario in this article, might include TravelConstraints, TravelerInformation, and FlightPreferences.
The primary difference between DTOs and Domain Objects is that DTOs contain neither business logic nor code for CRUD operations. DTOs are meant to be used as convenient “records” that carry information to and from consumers. As such, they may not map directly to objects in either the domain model or database schema. DTOs are specifically optimized for data transfer. They achieve this end by slimming down their payload.
DTOs may be comprised solely of primitive data types (e.g. int, string, double, etc.) or may contain any combination of primitive types and complex types. An example of a TravelContraints DTO is shown below …
[DataContract(Namespace = "http://WCF.SOAPatterns.DataContracts/2006/12", Name = "TravelConstraints")]
public partial class TravelConstraints
{
[DataMember(IsRequired=true, Order=1)]
public string StartCity;
[DataMember(IsRequired=true, Order=2)]
public string DestCity;
[DataMember(IsRequired=true, Order=3)]
public int Month;
[DataMember(IsRequired=true, Order=4)]
public int Day;
[DataMember(IsRequired=true, Order=5)]
public int Year;
}
Listing 1
You can see above that the TravelConstraints DTO only contains primitive types, but it could also contain other DTOs. If you want to ensure cross-platform interoperability, you must make sure that all the types within each of your DTOs are WS-I Basic Profile compliant. While this is relatively easy to do with WCF (re: Creating WS-I Basic Profile 1.1 Interoperable Services), you’ll have to trade off on faster WCF bindings.
You may have noted that the TravelConstraints DTO does not use properties. You may recall that properties are meant to provide a controlled mechanism to set or retrieve the values on an object’s fields; this is essentially a form of validation. We could justifiably assume that such validation logic will be implemented behind the “Service Façade” from within the business logic layer. This being said, if you feel that your classes should always implement properties, then it’s simple enough to apply the Encapsulate Field refactoring.
The following actions will cause breaking changes in your service DTOs. If you do any of these things, your consumers will need a new proxy (a.k.a. the WCF Client).
Contracts in the real world are comprised of three components. These include the offer, consideration, and acceptance. In the offer, the person proposing the contract presents the terms of a potential agreement that the parties would enter in to. The contract is not enforceable until the other party has a chance to consider what's being offered and accepts the terms, either explicitly or implicitly through their behavior. Therefore, with contract law, a contract does not exist without consideration and acceptance. Sometimes the contract is considered invalid if the accepting party doesn't understand the offer. This usually occurs when there is some ambiguity in the terms.
In the world of SOA, the offer occurs when a contract (e.g. described in WSDL) is formally published. It is at this point that the service provider is stating to all consumers, "These are the terms we're offering". The consumer may then consider if they want to work with the provider. Once the consumer accepts the terms of the contract, either explicitly by communicating their intent to the provider, or implicitly by simply using the service, the contract is complete.
The contract that is first offered may be viewed as being the “Base Contract”. Just as contracts in the real world can be amended, so too can we amend the contracts used in our services as long as we comply with a few rules. First, Contract Amendments must be offered as optional considerations. Consumers must not be required to abide by these amendments. If the new items in the contract are required, then the service provider must offer a new contract for the consumer to consider. This, in a nutshell, is the concept behind the Contract Amendment Pattern.
Let’s see how we can apply the Contract Amendment Pattern to DTOs. You may extend any DTO by adding DataMembers at the end of the DTO. You must set the IsRequired attribute to false, and the order attribute should also be set to be higher than prior items in the DTO. Consumers that use the older proxy should still be able to communicate with the service operations that use this amended DTO, and consumers that have the newer proxy will be able to take advantage of the contract amendment.
Continuing on with our example, let’s see how we could amend the TravelConstraints DTO. First, I’ll introduce an enumerated data type that will define the options that a traveler might want to select for the time of day they prefer to travel:
public enum TimeOfDayPreferences
AnyTime = 0,
Morning = 1,
Afternoon = 2,
Evening = 3
Listing 2
To apply the Contract Amendment pattern to the TravelConstraints DTO, simply add it to the end of the DTO as an optional element, and be sure to set the order value as well:
#region Base Contract: Version 1
#endregion
[DataMember(IsRequired=false, Order=6)]
public TimeOfDayPreferences TravelTimePreference;
Listing 3
Notice that I’ve demarcated the “base contract” within a region. This is done simply to help the reader see which portion of the DTO was included in the original contract.
There are several things to consider when you’re thinking of applying the Contract Amendment pattern. First, the services in which the amended DTO are used should probably be designed to understand both the older and newer versions of the DTO. If you’re receiving an amended DTO from a consumer that uses the older proxy, the values for the DataMembers that you added will always be null. The service may still instantiate these new items, populate them, and even send them back to that consumer without a hitch. In this scenario, WCF would put the new DataMembers into the ExtensionDataObject of the DTO.
You must also have some awareness as to whether or not the consumers of your service use strict or lax validation. If the consumer validates the schemas for the messages being passed back, we must know whether or not they will ignore new types added to the messages (i.e. our Contract Amendments). If they do not ignore new DTOs and throw exceptions when they encounter things they don’t recognize, then we say that the consumer is enforcing strict validation. In this case, you’ll have to work things out with your consumers. However, if the consumers of your service will tolerate new optional elements (i.e. they have lax validation), then the Contract Amendment would be a viable approach.
Before I describe this pattern I should state that this approach is very different from what you’ll see in most literature from Microsoft and from the current and popular books on WCF. Regardless, it is recognized by many as a best practice in service design.
Service messages are simply containers that wrap one or many DTOs. This entity defines the information that is passed to or from service operations and may be reused across a number of operations. In this pattern each service operation may contain at most two Service Messages. Most operations will implement the Request/Response Message Exchange Pattern and will therefore have one inbound Request Message and one outbound Response Message. This “Messaging Style” of communications is quite a bit different than what people may have become accustomed to when defining the RPC style of signature that is prevalent in web services using technologies like Asmx.
There are some very good reasons to use this message-based style of communications. Message-oriented operation signatures promote ease of extensibility, and also tend to give you much greater control over the message itself (e.g. the ability to add custom headers and security policies on message headers or bodies).
In order to make a case for the first benefit identified above, consider a service operation named GetFlightSchedules. Let’s say that this operation should take in two DTOs named departConstraints and returnConstraints. The natural inclination for many developers would be to define this operation as is shown in Listing 4.
[System.ServiceModel.ServiceContractAttribute(Namespace = "http://WCF.SOAPatterns.ServiceContracts/2006/12", Name = "IBargainAirService")]
public interface IBargainAirService
[OperationContractAttribute]
MatchingFlights GetFlightSchedules( TravelConstraints departConstraints, TravelConstraints returnConstraints);
Listing 4
If the GetFlightSchedules operation included multiple input parameters as is shown in Listing 4, we would not be able to easily extend that operation while maintaining backward compatibility. If we did want to add additional input or even return parameters in the future, we would essentially be forced to create a new operation which included all of the existing DTOs named above plus the new DTO we would like to append. We’d then have to determine what to call the new operation, what to do with the old operation (i.e. should it be deprecated?), and we might need to generate a new proxy for consumers. If the older operation were kept around, then we'd also have to determine if and how the older operation might work with the newer operation. Similar issues would need to be worked out if we later decided that we wanted to return more than just MatchingFlights.
If, however, we use a single input parameter that contains all of these DTOs, we will have the ability to extend the message itself without necessarily having to deal with some of the issues identified above. Listing 5 shows what this Service Contract would look like.
TravelOptionsMessage GetFlightSchedules(TravelOptionsMessage request);
Listing 5
If we decide to use the Service Message Pattern, we must consider how we might use WCF to implement it; our choices include MessageContracts and DataContracts. Unfortunately, DataContracts bloat your messages. Let’s find out why by looking at a Service Message implemented through DataContracts:
[DataContract]
public partial class TravelOptionsMessage
public WCF.SOAPatterns.DataContracts.TravelConstraints DepartConstraints;
[DataMember(IsRequired = true, Order = 2)]
public WCF.SOAPatterns.DataContracts.TravelConstraints ReturnConstraints;
[DataMember(IsRequired = true, Order = 3)]
public WCF.SOAPatterns.DataContracts.TravelOptions MatchingFlights;
Listing 6
Our end goal is to use TravelOptionsMessage as our Request and Response Message within the GetFlightSchedules operation. If we were to use a DataContract for this purpose, then the WSDL generated for GetFlightSchedules would appear as is shown in Listing 7.
Listing 7
Notice that the Service Message, TravelOptionsMessage, does not appear as either the input or output message to GetFlightSchedules. When DataContracts are used as parameters within WCF OperationContracts, WCF automatically wraps those parameters within its own messages that it generates from the name of the operation. You can see in Listing 8 how WCF generates a request message named GetFlightSchedules and a response message named GetFlightSchedulesResponse.
Listing 8
You can also see in Listing 8 that these auto-generated messages contain the custom Service Message named TravelOptionsMessage. The net result (no pun intended) of using DataContracts as Service Messages (i.e. as request or response messages) is that they will be wrapped by an extra and unnecessary message container.
What if we were to use MessageContracts to implement the Service Message pattern? Let’s first take a look at the Service Message implemented with this WCF construct:
[MessageContract]
[MessageBodyMember(Order=1)]
[MessageBodyMember (Order = 2)]
[MessageBodyMember (Order = 3)]
Listing 9
As you can see, it looks pretty similar to the code presented in Listing 6. The effect of using MessageContracts can be seen in Listing 10.
Listing 10
You can see that the operation for GetFlightSchedules is accompanied by the input and output messages, both of which are TravelOptionsMessage. The XSD for TravelOptionsMessage appears in Listing 11.
Listing 11
This is very good since we don’t get the double-wrapping effect that we saw when DataContracts were used. This certainly is the cleaner of the two approaches, but there is one more problem. The astute reader may have noticed in Listing 9 that the MessageContract does not provide for optional members. How then might we apply the Contract Amendment Pattern?
The answer lies in the creation of a specific DTO for this message that will serve as an “Extension Element”. The initial content of this DTO might look something like this:
public partial class TravelOptionsMessageExtension
[DataMember(IsRequired=false, Order=1)]
public string ExtensionPoint = “TravelOptionsMessageExtensions Follow”;
Listing 12
To allow for Contract Amendments in the TravelOptionsMessage, simply add this DTO to the end of the message:
[MessageBodyMember (Order = 4)]
public WCF.SOAPatterns.DataContracts.TravelOptionsMessageExtension Amendments;
Listing 13
If you ever want to apply the Contract Amendment Pattern to this message, you can simply add optional DTOs to the TravelOptionsMessageExtension DTO. Let’s find out how to do that now.
What if, over time, we decide that the GetFlightSchedules operation should optionally receive a customer’s FlightPreferences? This new DTO is shown in Listing 14.
[DataContract(Namespace = "http://WCF.SOAPatterns.DataContracts/2006/12", Name = "FlightPreferences")]
public partial class FlightPreferences
[DataMember(IsRequired = false, Name = "SeatPreference", Order = 1)]
public WCF.SOAPatterns.DataContracts.SeatPreferences SeatPreference;
[DataMember(IsRequired = false, Name = "TravelClass", Order = 2)]
public WCF.SOAPatterns.DataContracts.TravelClass TravelClass;
Listing 14
The FlightPreferences DTO contains two enumerated types whose definitions are displayed in Listing 15.
public enum SeatPreferences
Aisle = 0,
Window = 1
public enum TravelClass
Economy = 0,
Business = 1,
FirstClass = 2
Listing 15
We simply need to add this new DTO into the Extension Element defined for the Service Message. This is shown in Listing 16.
#region Contract Amendment: Version 2
[DataMember(IsRequired = false, Order = 2)]
public WCF.SOAPatterns.DataContracts.FlightPreferences FlightPreferences;
Listing 16
By using this approach, you will not need to create a new operation to take the place of GetFlightSchedules. Notice that this new DTO is optional and has an explicit order set to be higher than all prior DTOs in the Extension Element. This technique allows consumers that only know the older message to continue to work with the service operation without needing to receive a new proxy. This, in turn, will allow you to extend your messages at almost any time. Simply add the new optional DTO to the end of the Extension Elements within your Service Messages.
Here again you will need to determine if the operation should tolerate missing DTOs. This is really more of a business question than it is a technical concern. In this example, it probably is ok to tolerate missing FlightPreferences.
As was suggested for DTOs, you should become familiar with those actions that will cause breaking changes in the Service Messages. The following list highlights a few of these concerns:
If you do not have reason to incur breaking changes, then you should be able to put out a minor release of your service at the same endpoint (i.e. Address and Binding), and the consumers that want to keep working with the older proxies should be just fine. I’ll talk more about how to handle breaking changes later on in this article.
The Service Contract defines the operations (i.e. methods) that a given service will provide, the Messages used in each operation, and the Message Exchange Patterns (i.e. Simplex, Request/Response, Duplex) used by each operation as well. This is the entity that proclaims to all potential consumers the rules for usage. While it is possible to define this contract on a class that also implements the code for the service, we should define this contract independently and separately from the classes that are used to fulfill the implementation of that contract. The reasons for this include the following:
Listing 17 provides a Service Contract named IBargainAirService that is used by our fictional discount airline. You can see that each operation contains one request message and one response message. Once again, each of the Service Messages appearing as request or response messages may contain one or many DTOs.
CustomerMessage GetCustomerAccount(CustomerMessage request);
TripReservationMessage ReserveTrip(TripReservationMessage request);
Listing 17
There are several actions that may cause breaking changes to the Service Contract and would require consumers of your service to receive new proxies that reflect the latest definition of that contract. These actions include:
I’ll talk about what you will need to do if a breaking change can not be avoided later in this article. (Note: changing the endpoint address or bindings on a service can also cause breaking changes for consumers).
Fortunately, you can extend the Service Contract by adding new operations to it. This act will not cause breaking changes for consumers that have proxies that only know about the older operations.
You should think in terms of use-case packages (i.e. a set of use-cases that fall under a given category) when considering what operations should be included on a Service Contract. Each contract contains one or many operations that represent individual use-cases within those categories.
You should also try to define “coarse grained operations” on your Service Contracts and get as much done as you can in each call. Each service operation should represent a logical unit of work that stands alone. If operations adopt the ACID principle used by databases, then consumers won’t have to call multiple services to achieve a given end. This approach also encourages looser coupling, in the classic computer science sense, between consumers and services. The fewer operations a consumer deals with, the looser the ties to the service. Furthermore, if your services expose fewer operations and each service gets more done with each call, you will also tend to discourage “chatty communications” between your consumers and services. Given that each call to a service operation might incur a performance penalty because you don’t know where that service is located, it is wise to minimize the trips over to your service. This means that you should not take a fine-grained domain object with properties and simply “service-enable it”. Such an approach will only lead to higher coupling and performance that is less than optimal.
There is a danger of going too far with coarsely defined operations that attempt to get a lot done in any given call. The service might return too much data, in which case we have what I call the “Data Buffet Anti-Pattern”. Much like wild-eyed Las Vegas vacationers who sometimes overload their trays at the “All You Can Eat Buffet” and leave much of their food untouched, so too can services go too far when loading up their messages with data. It’s hard to say what’s the right amount of data a service should send or receive, but if you keep the “Data Buffet” idea in the back of your mind, you’ll have a guideline to determine what might be appropriate for your business requirements.
A final Anti-Pattern to look out for is “Database Bleed-Through”. This occurs when the details of your database show through into the Service Contract. You can recognize this when you simply have a service that performs CRUD operations against each table or View in your database (a.k.a. CRUDDy Interface). This also occurs if you create one service operation for each stored procedure. The problem with each of these approaches is that it creates a tight coupling between your Service Contract, which should be the doorway into your business logic, and the details of your database schema objects. If you ever want to alter the manner in which you write data to or read data from persistent data storage (e.g. if you want to change the designs of your tables, procedures, or even which DBMS you use), you may have to alter your Service Contract. If, instead, you think of your Service Contract as a doorway into your business logic and as a representation of some use-case in your problem domain, then you can see that the service may actually mediate calls to one or many business logic components that interact with one or many tables, views, or stored procedures. The end result is that the service consumer would have no need to understand the details of the database implementation, and would, in fact, not even know what is happening behind the service façade.
The Service Implementation is a class that fulfills the Service Contract with actual programmatic logic. When creating the code for your service implementation class, try to think of it as nothing more than a gateway into your business logic. You should not put the actual business logic in your service implementation. Instead, you should merely invoke methods on your business components from your services.
While the Service Contract defines what data is passed to and from the service, the Service Implementation class plays the role of Mediator and Façade. It is a mediator because it decides, directs, and mediates what business components should be called to achieve the goals of the service operation, and it is a façade because it hides the complexity of this mediation from all of its consumers. If this mediation or orchestration of business logic becomes somewhat complex, then one might argue that it should either be pushed entirely into a business logic component or encapsulated within a well defined orchestration directed by technologies such as Windows Workflow Foundation or an Enterprise Service Bus. Figure 2 shows how a Service Implementation class can mediate or forward delegate in this manner.
Figure 2
If you think of your services as gateways or doors that provide alternative ways to communicate with your business, and your business component layer as the place where your business logic is maintained, then you can see that each layer serves a distinct purpose which further promotes a separation of concerns.
It is not uncommon to require many ways of retrieving what is essentially the same set of data from a service. For example, consider the GetCustomerAccount operation shown in Listing 17. It is a normal practice for programmers to create one operation for each specific way to query on customer accounts. For example, many developers are prone to create methods like GetCustomerAccountById, GetCustomerAccountByLastName, GetCustomerAccountByCompanyName; the list could obviously go on and on. Unfortunately, this style would create a Service Contract that would be quite verbose, and would also tend to increase coupling.
Another approach would be to use a pattern similar to Martin Fowler’s Query Object. I call this the Search Criteria Pattern. This is simply a DTO that is provided as the first DTO in the Base Contract of your Request Message. Listing 18 shows a CustomerMessage that contains a SearchCriteria DTO.
public partial class CustomerMessage
public WCF.SOAPatterns.DataContracts.CustomerSearchCriteria SearchCriteria;
public WCF.SOAPatterns.DataContracts.CustomerDTO Customer;
Listing 18
Listing 19 shows the details of this search criteria class.
public partial class CustomerSearchCriteria
public int Id;
[DataMember(IsRequired=false, Order=2)]
public string LastName;
[DataMember(IsRequired=false, Order=3)]
public string CompanyName;
Listing 19
Hopefully you can see that this one DTO provides the consumer the means to search for customers by using any or all of its elements. If specific combinations of search parameters were required, you could create one DTO for each of those combinations, then add those new DTOs to the CustomerSearchCriteria class. If you discovered over time that you wanted to provide new search criteria to your consumers, you could easily apply the Contract Amendment pattern to the CustomerSearchCriteria DTO, and you would be able to maintain forward and backward compatibility of the same Service Contract and endpoint.
While the techniques described in this article should help you to create self-describing and explicit contracts that are also extensible, there will be times when you have no choice but to cause a breaking change in the Service Contract, Service Message, or DTOs. When you do need to introduce a breaking change, you should follow the steps below to roll out a major release of your service:
- Create a new namespace for your service and apply this to your Service Contract
eg. protocol://CompanyName/FunctionalArea/ServiceName/Date
- Deploy your service to a new address. This will, in effect, be a new endpoint (i.e. Address, Binding, and Contract)
- You may need to deprecate older operations
- You will have to alert consumers of your service that they will need to create a new proxy; perhaps you could be really nice and create one for them
Every technology choice has trade-offs, and WCF is no different. This article pointed out a few of the things you can do with WCF, but as we saw, each option forces you to accept some downsides. Those who work extensively with messaging systems also chide WCF for its inability to handle things like XSD:Choice and Open Content Models. Perhaps someday we’ll come up with some neat patterns to deal with those issues.