Tuesday 2 July 2013

WCF - MessageContracts VS DataContracts

WCF: Message Contracts Vs Data Contracts

Most of the situations where services oriented applications are developed, there is no real necessity to have granular control on the SOAP message exchanged between the client and the service. But then why do we at all have message contracts been provided then? Is there a difference between data contracts and message contracts? For this article, I would assume we all know the definitions of both and have an understanding of each contracts. What I will try to explain is what is the difference at the messaging level (SOAP request and response) and the way client calls and passes parameters in case of a service which use only datacontracts and only messagecontracts.

1) When we introduce a message contract into an operation signature, we must use a message contract as the only parameter type and as the only return type of the operation. Whereas in case of using a data contract, the operation parameter and return type could be mixed with the simple types and void. This means that there is no partial use of message contracts.

ex.
Operation signature when using MessageContracts:
[OperationContract]
        UserContactInfoResponse GetUserDetails(UserContactInfoRequest UserInfo);
where UserContactInfoResponse and UserContactInfoRequest are both MessageContracts.

Operation signature when using DataContracts:
[OperationContract]
        UserContactInfoResponse GetUserDetails(int UserId);
where UserContactInfoResponse is a DataContract

2) MessageContracts help us to control the structure of SOAP message body and in turn allows us to control how it is serialized. This is especially helpful when interoperability is essential because we have control on how the SOAP message would be serialized.

The following is the part of the SOAP response (body) in case of the operation signature used for MessageContracts above (IsWrapped attribute of the MessageContract is true hence the MessageContract class name, UserContactInfoResponse, is appearing directly under SOAP body tag. If IsWrapped would have been false, the memberes would be directly listed under the body tag) :

<s:Body u:Id="_0">
    <UserContactInfoResponse xmlns="http://tempuri.org/">
      <UserInformation xmlns:a="http://schemas.datacontract.org/2004/07/MessageContractExample" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
        <a:EmailAddress>info@microsoft.com</a:EmailAddress>
        <a:Name>Microsoft</a:Name>
      </UserInformation>
    </UserContactInfoResponse>
  </s:Body>


Note that both the tags, UserContactInfoResponse (Message contract class) and UserInformation (Message contract element/a field in the Message contract class) are used in the service and they appear in the SOAP response body.

Now using the same operation signature ie. UserContactInfoResponse GetUserDetails(UserContactInfoRequest UserInfo);, when using DataContracts (i.e. where UserContactInfoResponse and UserContactInfoRequest are both DataContracts), the following is the SOAP response body

<s:Body u:Id="_0">
    <GetUserDetailsResponse xmlns="http://tempuri.org/">
      <GetUserDetailsResult xmlns:a="http://schemas.datacontract.org/2004/07/DataContractExample" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
        <a:EmailAddress>info@microsoft.com</a:EmailAddress>
        <a:Name>Microsoft</a:Name>
      </GetUserDetailsResult>
    </GetUserDetailsResponse>
  </s:Body>

As can be seen, the tag, GetUserDetailsResponse, is a default tag generated after the response is serialized and we have no operation or datacontract defined by this name.

Hence, for the same operation signature, the SOAP response body is different but what is striking is that MessageContract provides us control over the SOAP body.

When the client makes the method call using the proxy, the datacontract expects the data passed identical to the structure defined. What it means is that the method call would expect an instance of

UserContactInfoRequest to be passed as the method parameter. But in the case of MessageContracts, the parameters which are passed in the called method are the fields of the MessageContract and not the instance of UserContactInfoRequest.

In the code, this is evident in the following lines:

Inside CallDataContractOperation(): Task<DC.UserContactInfoResponse> tempres = (new DC.GetUserDetailsServiceClient()).GetUserDetailsAsync(req);

Inside CallMesssageContractOperation(): Task<MC.UserContactInfoResponse> tempres = (new MC.GetUserDetailsServiceClient()).GetUserDetailsAsync(req.LicenseKey,req.ExtraInformation);


3) MessageContracts allow us to access SOAP heades and pass such information which we do not or may not wish to pass in the SOAP body. This could be a session information or an authentication token (ex. license key or user credentials) or it could be signing or encrypting SOAP header information.

The following is the SOAP request where I am passing the license key to validate the request in case of MessageContracts (Note that IsWrapped attribute is true):

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://tempuri.org/IGetUserDetailsService/GetUserDetails</a:Action>
    <h:LicenseKey xmlns:h="http://tempuri.org/">12345</h:LicenseKey>
    <a:MessageID>urn:uuid:bae90c7c-3869-4db9-9d5f-8307f27f9ff1</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
  </s:Header>
  <s:Body>
    <UserContactInfoRequest xmlns="http://tempuri.org/">
      <ExtraInformation>ExtraInformation</ExtraInformation>
    </UserContactInfoRequest>
  </s:Body>
</s:Envelope>

The following is the SOAP request when passing the license key as a parameter (by defining it as an int field in UserContactInfoRequest) in case of DataContracts:

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">http://tempuri.org/IGetUserDetailsService/GetUserDetails</a:Action>
    <a:MessageID>urn:uuid:c8eaf425-77bb-4505-9a2a-e0be1f6390f1</a:MessageID>
    <a:ReplyTo>
      <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>
    </a:ReplyTo>
  </s:Header>
  <s:Body>
    <GetUserDetails xmlns="http://tempuri.org/">
      <UserInfo xmlns:d4p1="http://schemas.datacontract.org/2004/07/DataContractExample" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
        <d4p1:ExtraInformation>ExtraInformation</d4p1:ExtraInformation>
        <d4p1:LicenseKey>12345</d4p1:LicenseKey>
      </UserInfo>
    </GetUserDetails>
  </s:Body>
</s:Envelope>

The code is as follows:

Code in project containing Message Contract:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace MessageContractExample
{

    [ServiceContract]
    public interface IGetUserDetailsService
    {
        [OperationContract]
        [FaultContract(typeof(string))]
        UserContactInfoResponse GetUserDetails(UserContactInfoRequest UserInfo);
    }

     [DataContract]
    public class UserContactInfo
    {
        [DataMember]
        public string Name
        {
            get;
            set;
        }

        [DataMember]
        public string EmailAddress
        {
            get;
            set;
        }
    }

    [MessageContract(IsWrapped = true)]
     public class UserContactInfoRequest
     {
        [MessageHeader]
        public int LicenseKey;

        [MessageBodyMember]
        public string ExtraInformation;
     }

    [MessageContract(IsWrapped = true)]
    public class UserContactInfoResponse
    {
        [MessageBodyMember]
        public UserContactInfo UserInformation;
    }

public class GetUserDetailsService : IGetUserDetailsService
    {
        private const int ValidLicenseKey = 12345;
        public UserContactInfoResponse GetUserDetails(UserContactInfoRequest UserInfo)
        {
            if (UserInfo.LicenseKey == ValidLicenseKey)
            {
                UserContactInfoResponse UserInfoResponse = new UserContactInfoResponse();
                UserInfoResponse.UserInformation = new UserContactInfo();
                UserInfoResponse.UserInformation.Name = "Microsoft";
                UserInfoResponse.UserInformation.EmailAddress = "info@microsoft.com";
                return UserInfoResponse;
            }
            else
            {
                const string ErrorMessage = "Invalid License Key";
                throw new FaultException<string>(ErrorMessage,new FaultReason("License Key is not valid"));
            }
        }
    }

}


Code in project containing Data Contract only and no Message Contract:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;

namespace DataContractExample
{
    [ServiceContract]
    public interface IGetUserDetailsService
    {
        [OperationContract]
        [FaultContract(typeof(string))]
        UserContactInfoResponse GetUserDetails(UserContactInfoRequest UserInfo);
    }

    [DataContract]
    public class UserContactInfoResponse
    {
        [DataMember]
        public string Name
        {
            get;
            set;
        }

        [DataMember]
        public string EmailAddress
        {
            get;
            set;
        }
    }

    [DataContract]
    public class UserContactInfoRequest
    {
        [DataMember]
        public int LicenseKey
        {
            get;
            set;
        }

        [DataMember]
        public string ExtraInformation
        {
            get;
            set;
        }
    }

public class GetUserDetailsService : IGetUserDetailsService
    {
        private const int ValidLicenseKey = 12345;
        public UserContactInfoResponse GetUserDetails(UserContactInfoRequest UserInfo)
        {
            if (UserInfo.LicenseKey == ValidLicenseKey)
            {
                UserContactInfoResponse UserInfoResponse = new UserContactInfoResponse();
                UserInfoResponse.Name = "Microsoft";
                UserInfoResponse.EmailAddress = "info@microsoft.com";
                return UserInfoResponse;
            }
            else
            {
                const string ErrorMessage = "Invalid License Key";
                throw new FaultException<string>(ErrorMessage, new FaultReason("License Key is not valid"));
            }
        }

    }

}

Client Code (dont forget to add the service references and change the service namespaces with the ones you provide):

using System;
using System.Threading.Tasks;
using DC = CallingWCFServices.DataContractNamespace;
using MC = CallingWCFServices.MessageContractNamespace;

namespace CallingWCFServices
{
    class Program
    {
        static void Main(string[] args)
        {
            CallDataContractOperation();
            CallMesssageContractOperation();
            Console.ReadLine();
        }

        public static async void CallDataContractOperation()
        {
            DC.UserContactInfoRequest req = new DC.UserContactInfoRequest();
            req.LicenseKey = 12345;
            req.ExtraInformation = "Extra Information";

            DC.UserContactInfoResponse res = new DC.UserContactInfoResponse();
            Task<DC.UserContactInfoResponse> tempres = (new DC.GetUserDetailsServiceClient()).GetUserDetailsAsync(req);
            res = await tempres;

            Console.WriteLine("DataContractOperation: Name = {0} and Email = {1}", res.Name, res.EmailAddress);
        }

        public static async void CallMesssageContractOperation()
        {
            MC.UserContactInfoRequest req = new MC.UserContactInfoRequest();
            req.LicenseKey = 12345;
            req.ExtraInformation = "Extra Information";

            MC.UserContactInfoResponse res = new MC.UserContactInfoResponse();
            Task<MC.UserContactInfoResponse> tempres = (new MC.GetUserDetailsServiceClient()).GetUserDetailsAsync(req.LicenseKey,req.ExtraInformation);
            res = await tempres;

            Console.WriteLine("MessageContractOperation: Name = {0} and Email = {1}", res.UserInformation.Name, res.UserInformation.EmailAddress);
        }
    }
}