Recommendations to design message contracts
These are some useful practices to design message contracts. They are based on my experience as developer on different software projects.
- Do not include business behavior on the messages
- Do not use datasets as messages
- Be aware of any extensibility point
Do not include business behavior on the messages
Always use simple DTO (Data Transfer Object) classes to transfer data between the web service and the client.
Here, a DTO is a object that represents the data in a simple format whose only purpose is to transfer the data. There is no business logic and no database logic in these objects, just data.
These will also map more closely to XML, and so will be likely to be compatible between the client and the service.
This allows you to use whatever business-layer or data layer you like inside of the service, yet to only present the data itself to the clients.
A DTO could contain some serialization logic when the mapping between the object state and XML is as not simple as it should be. This logic is usually implemented through the IXMLSerializable Interface, which provides the methods to serialize and deserialize an object to XML.
I am not saying that a DTO must not contain any method, it actually can have some helper methods, but those methods are not going to be used by the client application. This is because the client will only receive a XML copy of the DTO object, it is not going to receive an instance of the DTO implementation class. (For instance, a .NET class).
For example, we can have a method to sum up the total amount of an purchase order, this method basically enumerates each item in the order and adds the item amount to the total amount. As I said before, this method will only be available on the service side.
1 [XmlRoot("Order")]
2 public class Order
3 {
4 private OrderItem[] _items;
5
6 public Order()
7 {
8 }
9
10 [XmlArray("Items")]
11 [XmlArrayItem("Item")]
12 public OrderItem[] Items
13 {
14 get { return _items; }
15 set { _items = value; }
16 }
17
18 public decimal GetOrderTotal()
19 {
20 decimal total = 0;
21 foreach (OrderItem item in _items)
22 {
23 total += item.Amount;
24 }
25
26 return total;
27 }
28 }
29
30 public class OrderItem
31 {
32 private int _id;
33 private decimal _amount;
34
35 public OrderItem()
36 {
37 }
38
39 [XmlAttribute("Id")]
40 public int Id
41 {
42 get { return _id; }
43 set { _id = value; }
44 }
45
46 [XmlAttribute("Amount")]
47 public decimal Amount
48 {
49 set { _amount = value; }
50 get { return _amount; }
51 }
52 }
I know, you can modify the auto-generated proxy class on the client side( Usually, this class is auto generated by a tool using the service's WSDL. .NET and Java provide similar tools to perform this task ) to use the DTO implementation class instead of the auto-generated messages, but that goes against the interoperability principle of a web service. What happens if you need to support a new client platform in the future, for instance a PHP or action script client, are you going to implement the DTO in those platforms as well ?. Or if you simply change something in the message contract, you will have to deploy this change on every client application again.
You can have similar problems when you try to expose a business entity like an NHibernate entity as a message for a web service. As I described before, the client will only receive an entire XML copy of the object graph, it is not going to receive an instance of business entity itself (And all its methods)
This can be much worse if the design of business entity involves a relationship with many tables, since you can practically send a copy of entire database to the client application. This kind of class also presents serialization problems, they usually expose properties with types that can not be easily translated to XML.
In this case, you will need an additional layer to transform or map the business objects to DTOs. This could easily be done on the service interface layer. The DTOs should not necessarily have the same properties or granularity level of the business object. For instance, a business entity with the following structure,
1 public class OrderStatus
2 {
3 private string _id;
4 private string _description;
5
6 public OrderStatus()
7 {
8 }
9
10 public string Id
11 {
12 get { return _id; }
13 set { _id = value; }
14 }
15
16 public string Description
17 {
18 get { return _description; }
19 set { _description = value; }
20 }
21 }
22
23 public class Order
24 {
25 private List<OrderItem> _items = new List<OrderItem>();
26 private OrderStatus _status;
27
28 public Order(OrderStatus status)
29 {
30 _status = status;
31 }
32
33 public OrderStatus Status
34 {
35 get { return _status; }
36 }
37
38 public IList<OrderItem> Items
39 {
40 get { _items; }
41 }
42 }
could be represented in a different way
1 [XmlRoot("Order")]
2 public class Order
3 {
4 private string _statusId;
5 private string _statusDescription;
6 private OrderItem[] _items;
7
8 public Order()
9 {
10 }
11
12 [XmlArray("Items")]
13 [XmlArrayItem("Item")]
14 public OrderItem[] Items
15 {
16 get { return _items; }
17 set { _items = value; }
18 }
19
20 [XmlElement("StatusId")]
21 public string StatusId
22 {
23 get { return _statusId; }
24 set { _statusId = value; }
25 }
26
27 [XmlElement("StatusDescription")]
28 public string StatusDescription
29 {
30 get { return _statusDescription; }
31 set { _statusDescription = value; }
32 }
33 }
Do not use Datasets
Datasets provide a bunch of cool features for data management, such as data ordering or data filtering. They are also great to transport data between internal application layers.
However, they are an evil when we talk about interoperability with other platforms. The XML structure of a dataset is quite complex and really hard to represent in other platforms different from .NET. (Without mentioning the size of the payload generated after their serialization)
If your service is going to be consumed by different kind of client platforms, or that is something certainly unknown, use a DTO instead.
Be aware of any extensibility point
You can not predict offhand any change on the service requirements, but you can minimize the risks adding extensibility points to the messages.
For instance, using open XSD schemas or supporting a "any element/attribute" collection.
WCF also supports this concept through the interface IExtensibleDataObject.
1 [DataContract]
2 public class MyDataContract : IExtensibleDataObject
3 {
4 private ExtensionDataObject extensionData;
5
6 public ExtensionDataObject ExtensionData
7 {
8 get
9 {
10 return this.extensionData;
11 }
12
13 set
14 {
15 this.extensionData = value;
16 }
17 }
18 }