Programming Indigo: Contracts

 

David Pallmann

July 2005

Summary: In this chapter from his upcoming book, Programming Indigo, David Pallmann discusses how service, data, and message contracts are created in order to build interoperable services. (50 printed pages)

Contents

Understanding Contracts
Programming Service Contracts
Programming Data Contracts
Programming Message Contracts
Programming Exercise: Contracts
Summary

"What usually comes first is the contract."

—Benjamin Disraeli

Contracts are what make interoperable services possible. In this chapter, we'll go into detail about the three kinds of contracts in Indigo: service contracts, data contracts, and message contracts.

After completing this chapter, you will:

  • Understand the types of contracts available in Indigo and their purposes.
  • Know how to define service contracts.
  • Know how to define data contracts.
  • Know how to define message contracts.

Understanding Contracts

Contracts are one of the fundamental concepts in Indigo. They allow clients and services to have a common understanding of available operations, data structures, and message structures while remaining loosely coupled and platform independent. Indigo includes three kinds of contracts:

  • Service contract Describes the operations a service can perform
  • Data contract Describes a data structure
  • Message contract Defines what goes where in a message

You define contracts by using familiar object-oriented constructs: interfaces and classes. By annotating interfaces and classes with attributes, you bestow contract status on them.

Figure 5-1 illustrates the relationship of the three contract types to the common language runtime (CLR). All three types of contracts translate between Microsoft .NET types used internally and the XML representations shared externally:

  • A service contract converts between the CLR and Web Services Description Language (WSDL).
  • A data contract converts between the CLR and XML Schema Definition (XSD).
  • A message contract converts between the CLR and Simple Object Access Protocol (SOAP).

Aa480202.progindigoch5_01(en-us,MSDN.10).gif

Figure 5-1. Relationship of contracts to the common language runtime

Understanding Service Contracts

A service contract describes the operations a service can perform. A service must have at least one service contract, and it can have more than one. You can think of a service contract as follows:

  • It describes the client-callable operations (functions) your service provides.
  • It maps the interface and methods of your service to a platform-independent description.
  • It describes message exchange patterns that the service can have with another party. Some service operations might be one-way; others might require a request-reply pattern.
  • It is analogous to the <portType> element in WSDL.

You define a service contract by annotating an interface (or class) with the [ServiceContract] attribute. You identify service operations by placing [OperationContract] attributes on the methods of the interface. The following service contract defines three service operations.

[ServiceContract]
public interface IReservation
{
    [OperationContract]
    string StartReservation(string lastName, string firstName);
    [OperationContract]
    bool ReserveRoom(string arriveDate, string departDate, int guests);
    [OperationContract]
    string ConfirmReservation(bool authorize);
}

Once you define a service contract using an interface, you can write a class to implement the interface. For example:

public class Reservation : IReservation
{
    public string StartReservation(string lastName, string firstName)
    {
        ...
    }
    public bool ReserveRoom(string arriveDate, string departDate, int guests)
    {
        ...
    }
    public string ConfirmReservation(bool authorize)
    {
        ...
    }
}

If you don't like using interfaces, you can instead define the service contract directly against the implementation class and skip the interface altogether. The following class both defines and implements a service contract.

[ServiceContract]
public class Reservation
{
    [OperationContract]
    public string StartReservation(string lastName, string firstName)
    {
        ...
    }
    [OperationContract]
    public bool ReserveRoom(string arriveDate, string departDate, int guests)
    {
        ...
    }
    [OperationContract]
    public string ConfirmReservation(bool authorize)
    {
        ...
    }
}

A service contract can specify requirements that must be satisfied by endpoint bindings. The [BindingRequirements] attribute specifies binding requirements for the contract. The following service contract specifies a requirement for ordered delivery of messages.

[ServiceContract]
[BindingRequirements(RequireOrderedDelivery=true)]
public interface IReservation
{
    [OperationContract]
    string StartReservation(string lastName, string firstName);
    [OperationContract]
    bool ReserveRoom(string arriveDate, string departDate, int guests);
    [OperationContract]
    string ConfirmReservation(bool authorize);
}

The [ServiceContract], [BindingRequirements], and [OperationContract] attributes contain many parameters for specifying service contract details. For detailed information on these attributes, see the section titled "Programming Service Contracts" later in this chapter.

Types of Services

There are three types of services: typed, untyped, and typed message. Typed services are the simplest. Service operations of this type resemble the methods of a class and can accept or return both simple and complex data types. This is often the logical type of service to use in application programming.

    [OperationContract]
    Contact LookupContact(string firstName, string lastName);

Untyped services allow developers to work at the message level. Service operations of this type accept or return Message objects. Use this type of service when you need to work directly with messages.

    [OperationContract]
    Message ProcessMessage(Message request);

Typed message services fall between typed services and untyped services. Service operations of this type accept or return information using custom message classes defined with message contracts. Use this type of service when you want to be able to treat requests and responses as messages and as structured data.

    [OperationContract]
    MyResponseMessage ProcessMessage(MyRequestMessage request);

Understanding Data Contracts

A data contract describes a data structure. If you're passing more than simple types to or from a service operation, you must define a data contract. You can think of a data contract in these ways:

  • It describes the external format of information passed to and from service operations.
  • It defines the structure and types of data exchanged in service messages.
  • It maps a CLR type to an XML Schema.
  • It defines how data types are serialized and deserialized.
  • It is a versioning system that allows you to manage changes to structured data.

Serialization is the process of converting structured data such as an object into a sequence of bytes that can be transmitted over a network. Deserialization is the reverse process of reassembling structured data from a received sequence of bytes.

You define a data contract by annotating a class, structure, or enumeration with the [DataContract] attribute. You identify members of a data contract by placing [DataMember] attributes on the fields or properties of the class. The following code defines a data contract for a Contact class.

[DataContract]
public class Contact
{
    [DataMember] public string FirstName;
    [DataMember] public string LastName;
    [DataMember] public string Address;
    [DataMember] public string Address2;
    [DataMember] public string Phone;
    [DataMember] public string EMail;
}

Once a data contract is defined, you can use the type in service operations. The following code shows a service contract whose service operations send or receive the Contact structure described earlier.

[ServiceContract]
public interface IContact
{
    [OperationContract]
    Contact LookupContact(string lastName, string firstName);
    [OperationContract]
    void AddContact(Contact contact);
    [OperationContract]
    void UpdateContact(Contact contact);
    [OperationContract]
    void DeleteContact(Contact contact);
}

In a data contract, you must explicitly identify each member of the contract by using a [DataMember] attribute. This requirement ensures that developers make a conscious choice to expose data externally. It is the sole determinant of whether data is shared externally; access modifiers such as public and private do not play a role.

Each data contract must have a unique name—a namespace and name that can be specified as parameters of the [DataContract] attribute. By default, the namespace and name are derived from the class.

The [DataContract] and [DataMember] attributes contain many parameters for specifying data contract details. For detailed information on these attributes, see the section titled "Programming Data Contracts" later in this chapter.

Explicit vs. Implicit Data Contracts

Data contracts can be explicit or implicit. You define an explicit data contract by using the [DataContract] and [DataMember] attributes.

Simple types have an implicit data contract. For example, you can use an int or a string in a service operation without having to define a data contract. Here are the types with an implicit data contract:

  • System.Boolean
  • System.Byte
  • System.Char
  • System.DateTime
  • System.Decimal
  • System.Double
  • System.Int16
  • System.Int32
  • System.Int64
  • System.Object
  • System.SByte
  • System.Single
  • System.String
  • System.UInt16
  • System.UInt32
  • System.UInt64
  • Any delegate
  • Any enum
  • Any ISerializable type
  • Any [Serializable] type
  • Any array or generic of the preceding types

Serializable types also have an implicit data contract. Classes that implement .NET serialization using the [Serializable] attribute or the ISerializable interface can be passed to or from service operations without the need to specify data contract attributes.

Data Contract Compatibility

A data contract can be implemented in multiple ways. For a data contract to be considered compatible between two parties, its namespace, name, and list of members must match.

Consider the following simple data contract. The contract is named Bid and contains two members, Lot and Amount. By default, these names are taken from the class members.

[DataContract(Namespace="https://www.contoso.com")]
public class Bid
{
    [DataMember]
    public int Lot;
    [DataMember]
    public float Amount;
}

Another program might implement the same contract quite differently. The following class contains the same data contract but with different implementation details.

[DataContract(Namespace="https://www.contoso.com", Name="Bid")]
public class BidClass
{
    [DataMember(Name="Lot")]
    private string _lot;
    [DataMember(Name="Amount")]
    private double _amt;
    private int temp;
}

As in the first data contract, the contract name is Bid and the members are named Lot and Amount. Unlike in the first data contract, the contracts and members are explicitly given names that don't match the member names of the class. The data types and access modifiers are also different in this class. Despite these differences, the data contract is the same, and this information can be passed between two programs as a Bid.

It's perfectly acceptable for implementations to use different data types to implement data members, as long as a straightforward conversion exists. In the two contract implementations just shown, Lot and Amount are both implemented with different data types.

Data Contract Versioning

Data contracts support the concept of versioning. As data contracts are amended over time, you can use [DataMember] parameters to specify version numbers and control what happens if two parties are using different versions of a data contract. The following data contract has been amended several times.

[DataContract]
public class Book
{
    [DataMember]
    public string Title;
    [DataMember]
    public string Author;
    [DataMember]
    public string ISBN;
    [DataMember(VersionAdded=2, IsOptional=false, MustUnderstand=true)]
    public DateTime PublicationDate;
    [DataMember(VersionAdded=3, IsOptional=true, MustUnderstand=false)]
    public int Edition;
}

You can update a data contract without breaking compatibility by respecting the following versioning rules:

  • Don't change the namespace or name of the contract.
  • Don't change the names of the data members.
  • When adding new data members, specify a version number that is higher than previous version numbers.
  • Remove only data members that were defined to be optional.

Understanding Message Contracts

A message contract describes the structure of a message. Left to its own devices, Indigo makes its own decisions about what goes where in a SOAP message. Message contracts allow you to precisely control whether information goes in the message body or in message headers. One reason you might need this control is interoperability. Message contracts allow you to match the expectations of other systems concerning the structure of SOAP messages.

You define a message contract by annotating a class with the [MessageContract] attribute. You specify the message specifics for the class members by using the [MessageBody] and [MessageHeader] attributes. The following code defines a simple message contract.

[MessageContract]
public sealed class OrderItemRequest
{
    [MessageHeader]
    public string orderNo;
    [MessageBody]
    public OrderItem orderItem;
}

A custom message class is called a typed message. Once you define a typed message, you can pass the message to or from service operations.

[ServiceContract]
public interface ISampleContract
{
    [OperationContract(IsOneWay=true)]
    void ProcessMessage(OrderItemRequest message);
}

Typed messages combine the benefits of typed services and untyped services. As with typed services, typed message programming provides ease of use, doesn't require you to manipulate a message's XML Infosets directly, and provides fine control over the mapping of the data to the message structure.

Another place where you can use message contract attributes is in service contracts. You might have noticed this if you've studied code generated by the Svcutil tool. The following code shows message contract attributes in a service contract.

[System.ServiceModel.ServiceContractAttribute()]
public interface ILibrary
{
    [System.ServiceModel.OperationContractAttribute(Action = 
       "http://tempuri.org/ILibrary/FindBook", ReplyAction = 
       "http://tempuri.org/ILibrary/FindBookResponse")]
    [return: System.ServiceModel.MessageBodyAttribute(Name = "FindBookResult", 
       Namespace = "http://tempuri.org/")]
    int FindBook([System.ServiceModel.MessageBodyAttribute(Namespace = 
       "http://tempuri.org/")] ProgrammingIndigo.Book book);
}

The [MessageContract], [MessageHeader], and [MessageBody] attributes contain many parameters for specifying message contract details. For detailed information on these attributes, see the section titled "Programming Message Contracts" later in this chapter.

Programming Service Contracts

You define a service contract by annotating an interface or class with the [ServiceContract] attribute. You indicate service operations by using [OperationContract] attributes on the methods of the interface. You can also specify a [BindingRequirements] attribute for the interface.

[ServiceContract]
public interface ISpellChecker
{
    [OperationContract]
    void OpenDocument(string documentText);
    [OperationContract]
    string[] CheckSpelling();
    [OperationContract]
    void MakeCorrection(int location, string replacementText);
    [OperationContract]
    string CloseDocument();
}

Service contracts can inherit from each other.

The [ServiceContract] Attribute

The [ServiceContract] attribute marks an interface or class as a service contract.

[ServiceContract(Session=true, FormatMode=XmlSerializer)]
public interface IMyService
{
    ...
}

You can specify the following parameters in any order for the [ServiceContract] attribute:

  • CallbackContract (type)
    References a client callback contract used for duplex communication. The default is no callback contract.
  • FormatMode (ContractFormatMode)
    Selects a serializer. A serializer is a class used to perform serialization and deserialization. You can set this property to XmlFormatter or XmlSerializer. The default is XmlFormatter.
  • Name (string)
    Specifies the name of the service contract. If this parameter is omitted, the default is the interface name.
  • Namespace (string)
    Specifies the namespace of the service contract. If this parameter is omitted, the default is the interface namespace.
  • Session (boolean)
    Indicates whether the contract requires a session. The default is false.
  • Style (ServiceOperationStyle)
    Determines how the WSDL metadata for the service is formatted. Possible values are DocumentWrapped, DocumentBare, and Rpc. The default is DocumentWrapped. This property also exists at the [OperationContract] level.
  • Use (ServiceOperationBindingUse)
    Determines whether the message format is literal or encoded. Possible values are Literal and Encoded. The default is Literal. This property also exists at the [OperationContract] level.

CallbackContract: Defining a Duplex Service Contract

When you use the duplex messaging pattern, both the service and the client implement a contract of service operations. The client's contract is called the callback contract. Thus, for duplex messaging a pair of contracts is always required.

To define a duplex contract pair, you define two sets of interfaces as service contracts and then link them. The [ServiceContract] attribute on the service contract should contain a CallbackContract=typeof(type) parameter that references the client callback contract.

[ServiceContract(CallbackContract=typeof(ISampleClientContract))]
public interface ISampleContract
{
    [OperationContract(IsOneWay=false)]
    void MoveForward(int units);
    [OperationContract(IsOneWay=false)]
    void MoveBackward(int units);
    [OperationContract(IsOneWay=false)]
    void TurnLeft();
    [OperationContract(IsOneWay=false)]
    void TurnRight();
}

public interface ISampleClientContract
{
    [OperationContract(IsOneWay=false)]
    void Position(int x, int y, string direction);
}

If the client and service implement identical contracts, there is no need to define a second contract. The service contract can reference itself in the CallbackContract parameter.

[ServiceContract(CallbackContract=typeof(IChat))]
public interface IChat
{
    [OperationContract]
    void Say(string from, string text);
}

FormatMode: Selecting a Serializer

The serialization of service operation parameters and return values to messages and the subsequent deserialization are handled by a serializer. The default serializer is XmlFormatter. The other choice is the .NET Framework's XmlSerializer.

You can specify a serializer in the [ServiceContract] attribute by using the FormatMode parameter. The choices are ContractFormatMode.XmlFormatter and ContractFormatMode.XmlSerializer. The following service contract attribute specifies XmlSerializer.

[ServiceContract(FormatMode=ContractFormatMode.XmlSerializer)]

If you serialize using XmlSerializer, only public members of classes are serialized.

Name and Namespace: Naming a Service Contract

By default, a service contract takes its name and namespace from the interface or class that the [ServiceContract] attribute is applied to. To set a different name, use the Name parameter of [ServiceContract]. To set a different namespace, use the Namespace parameter.

[ServiceContract(Namespace="http://tempuri.org", Name="IMyContract"]
interface IMyPrototypeContract
{
    ...
}

Session: Requiring Sessions

A service contract can indicate that sessions and message ordering must be supported by its binding. You set the Session parameter of [ServiceContract] to true to require sessions.

[ServiceContract(Session=true)]
public interface IStockAssistant
{
    [OperationContract(IsInitiating=true)]
    void StartSession(string account);
    [OperationContract]
    void Buy(string symbol, int shares);
    [OperationContract]
    void Sell(string symbol, int shares);
    [OperationContract(IsTerminating=true)]
    void EndSession();
}

Style: Controlling Metadata Style

The way WSDL formats metadata for the service can be set to document/wrapped, document/bare, or RPC message style. The default is document/wrapped. To change the style, use the Style parameter at the [ServiceContract] or [OperationContract] level. Possible values are ServiceOperationStyle.DocumentWrapped, ServiceOperationStyle.DocumentBare, and ServiceOperationStyle.Rpc. The following code sets the metadata style to document/bare for the service contract in general, but one operation specifies the RPC style.

[ServiceContract(Style=ServiceOperationStyle.DocumentBare)]
public interface IMyService
{
    [OperationContract]
    int OperationA();
    [OperationContract]
    int OperationB();
    [OperationContract(Style=ServiceOperationStyle.Rpc)]
    int OperationC();
}

Use: Specifying Message Format

Messages can be in either literal or encoded format. In literal format, the body of a message conforms to an XSD. In encoded format, SOAP encoding is used. The default is literal. If you use the encoded setting, you must also specify XmlSerializer.

To change the message format, you set the Use parameter at the [ServiceContract] or [OperationContract] level. Possible values are ServiceOperationBindingUse.Literal and ServiceOperationBindingUse.Encoded.

The following service contract attribute sets the message format to Encoded and specifies the XmlSerializer serializer.

[ServiceContract(Use=ServiceOperationBindingUse.Encoded, FormatMode= 
ContractFormatMode.XmlSerializer)]

The [OperationContract] Attribute

The [OperationContract] attribute marks a method as a service operation and makes it part of the service contract.

[ServiceContract(Session=true, FormatMode=XmlSerializer)]
public interface IMyService
{
    [OperationContract(IsOneWay=true)]
    void SubmitExpenseReport(ExpenseReport report);
}

The following parameters can be specified for the [OperationContract] attribute:

  • Action (string)
    Identifies the action to be performed by the service operation. If Action is not specified, the default is http://tempuri.org/contract-name/operation-name. In an untyped service, you can set Action to * to intercept all service operations.
  • AsyncPattern (boolean)
    Defines the service operation as synchronous or asynchronous. An asynchronous service operation is implemented as a BeginX/EndX pair in accordance with the general Async Pattern of the .NET Framework. The default is false (synchronous).
  • IsInitiating (boolean)
    Determines whether a service operation initiates a new channel on the service. The default is true (yes).
  • IsOneWay (boolean)
    Defines the service as one-way or two-way. A one-way service does not send a response and might not have a return type other than void. A two-way service is accessed in a request-reply pattern. The default is false (two-way).
  • IsTerminating (boolean)
    Determines whether a service operation will close and shut down the client channel after the service operation completes and replies. The default is false (no).
  • Name (string)
    Specifies the name of the service operation. The default is the method name of the service operation.
  • ReplyAction (string)
    Identifies the action of the response sent by the service operation. If ReplyAction is not specified, the default is http://tempuri.org/contract-name/operation-nameResponse.
  • Style (ServiceOperationStyle)
    Specifies how the WSDL metadata for the service is formatted. Possible values are DocumentWrapped, DocumentBare, and Rpc. The default is DocumentWrapped. This property also exists at the [ServiceContract] level and is described in that section of the chapter.
  • Use (ServiceOperationBindingUse)
    Specifies whether the message format is literal or encoded. Possible values are Literal and Encoded. Use defaults to Literal. This property also exists at the [ServiceContract] level and is described in that section of the chapter.

Action: Specifying Actions

Every service operation has a name in the service contract called an action. Action is a URI. If you don't specify an action for a service operation, the name will be http://tempuri.org/contract-name/operation-name (for example, http://tempuri.org/IWeatherService/ReportLocalConditions).

You can specify an action using the Action=string parameter of the [OperationContract] attribute. You can specify a simple operation name, like RequestReply, or a full URI.

[OperationContract(Action="http://tempuri.org/ISomeContract/SomeOperation")]

In an untyped service operation, you can set Action to * to intercept all messages.

[ServiceContract]
public interface IMyUntypedContract
{
    [OperationContract(IsOneWay=false, Action="*")] 
    Message ProcessMessage(Message message);
}

AsyncPattern: Specifying Asynchronous Service Operations

Service operations can be synchronous or asynchronous. A client calling a two-way service operation synchronously blocks, waiting for the service operation to complete and send its response. A client calling a service operation asynchronously doesn't have to wait—it can start a service operation, do other work, and get the service operation result later.

Asynchronous service operations use the Async Pattern of the .NET Framework. Both clients and services can use the Async Pattern. For service operations, using AsyncPattern means breaking up the service operation into Begin<operation> and End<operation> operations. In the service contract, these two operations are defined under a single [OperationContract] attribute with an AsyncPattern=true parameter, like this:

[OperationContract(AsyncPattern=true)]
IAsyncResult BeginWeatherReport(string region, int temperature, int windSpeed, 
   int humidity, float pressure, AsyncCallback callback, object state);
void EndWeatherReport(IAsyncResult ar);

The Begin operation has two additional parameters beyond what the service operation normally requires: AsyncCallback and a state object. The Begin operation returns an IAsyncResult rather than what the service operation would normally return. The End operation accepts an IAsyncResult parameter and returns the service operation result.

For information on using the Async Pattern in clients, see Chapter 6. For information on implementing the Async Pattern in service operations, see Chapter 7.

IsInitiating and IsTerminating: Controlling Session Lifetime

Services can provide session instancing, in which a separate instance of the class is maintained for each client channel. The IsInitiating and IsTerminating parameters allow you to control which service operations initiate or terminate a session instance. By default, all service operations initiate a session instance. In the following service contract, the OpenAccount service operation initiates the sessions and must be the first service operation a client calls. The CloseAccount service terminates the session.

[ServiceContract]
public interface IBanking
{
    [OperationContract(IsInitiating=true, IsTerminating=false)]
    void OpenAccount(account);
    [OperationContract(IsInitiating=false, IsTerminating=false)]
    void Deposit(decimal amount);
    [OperationContract(IsInitiating=false, IsTerminating=false)]
    void Withdraw(decimal amount);
    [OperationContract(IsInitiating=false, IsTerminating=true)]
    void CloseAccount();
}

IsOneWay: Setting Service Operation Direction

A service operation can be one-way or two-way. By default, a service operation is two-way, meaning that when a service operation is accessed there is both an incoming request communication and an outgoing reply communication. Even if a service operation has a return type of void, a reply takes place if it is two-way. When a client invokes a two-way service operation, it waits for the reply before continuing with its execution. When a client invokes a one-way service operation, it does not wait for a response.

To specify direction, use the IsOneWay=boolean parameter of the [OperationContract] attribute. A value of true indicates a one-way service; false indicates a two-way service. The following service contract specifies both one-way and two-way service operations.

[ServiceContract]
interface IWeatherReporting
{
    [OperationContract(IsOneWay=true)]
    void ReportTemperature(int region, float temp);
    [OperationContract(IsOneWay=true)]
    void ReportWindspeed(int region, int windspeed);
    [OperationContract(IsOneWay=false)]
    float GetTemperature(int region);
}

Name: Naming a Service Operation

By default, a service operation takes its name from the method name the [OperationContract] attribute is applied to. To set a different name, use the Name parameter of [OperationContract].

[ServiceContract]
interface IMyPrototypeContract
{
    [OperationContract(Name="FunctionB")]
    void FunctionA();
}

ReplyAction: Specifying Actions

Just as the request message for a service operation specifies an action, a reply message specifies a reply action. A reply action is a URI. If you don't specify a reply action for a service operation, it will be named http://tempuri.org/contract-name/operation-nameResponse (for example, http://tempuri.org/IWeatherService/ReportLocalConditionsResponse).

You can specify a reply action by using the ReplyAction=string parameter of the [OperationContract] attribute. You can specify a simple operation name, like RequestReplyResponse, or a full URI.

[OperationContract(ReplyAction="http://tempuri.org/ISomeContract/SomeOperationResponse")]

The [BindingRequirements] Attribute

The [BindingRequirements] attribute specifies requirements for a service contract that bindings must meet. An error occurs if an attempt is made to create an endpoint whose binding doesn't match the requirements. Like the [ServiceContract] attribute, [BindingRequirements] is applied to the interface or class that defines the service contract.

[ServiceContract(Session=true)]
[BindingRequirements(
    CommunicationProfileRequirements=
      CommunicationProfileRequirements.DotNet,
    TransactionFlowRequirements=RequirementsMode.Require,
    QueuedDeliveryRequirements=RequirementsMode.Require,
    RequireOrderedDelivery=true)]
{
    ...
}

The following parameters can be specified for the [BindingRequirements] attribute:

  • CommunicationProfileRequirements (CommunicationProfileRequirements)
    Identifies a specific binding profile that must be used. Possible values are BasicProfile, WS, DotNet, and None. The default is CommunicationProfileRequirements.None, which allows bindings of any profile to be used.
  • QueuedDeliveryRequirements (RequirementsMode)
    Specifies whether queuing is required, prohibited, or permitted. Possible values are Required, Disallowed, and Ignore. The default is RequirementsMode.Ignore, which specifies no queuing requirements.
  • RequireOrderedDelivery (boolean)
    Specifies whether ordered delivery of messages is required. Possible values are true and false. The default is false, which means ordered delivery is not required.
  • TransactionFlowRequirements (RequirementsMode)
    Specifies whether transaction flow is required, prohibited, or permitted. Possible values are Required, Disallowed, and Ignore. The default is RequirementsMode.Ignore, which specifies no transaction flow requirements.

CommunicationProfileRequirements: Requiring a Profile

By default, bindings for a service contract can be of any profile: BasicProfile, WSProfile, NetProfile, or custom. To require a specific profile, use the CommunicationProfileRequirements parameter of [BindingRequirements]. Possible values are BasicProfile, WS, DotNet, and None. The default is None.

[ServiceContract]
[BindingRequirements(CommunicationProfileRequirements=CommunicationProfileRequirements.DotNet)]
Public interface IMyContract
{
    [OperationContract]
    void DoSomething();
}

QueuedDeliveryRequirements: Queuing Control

By default, bindings for a service contract are free to use the MSMQ transport or not. To specifically require or disallow the use of queuing in a binding, use the QueuedDeliveryRequirements parameter of [BindingRequirements]. Possible values are Required, Disallowed, and Ignore. The default is Ignore.

[ServiceContract]
[BindingRequirements(QueuedDeliveryRequirements=RequirementsMode.Required)]
Public interface IMyContract
{
    [OperationContract]
    void DoSomething();
}

RequireOrderedDelivery: Ordered Delivery Control

By default, bindings for a service contract can provide ordered delivery assurances or not. To specifically require or disallow ordered delivery, use the RequireOrderedDelivery parameter of [BindingRequirements]. Possible values are true and false. The default is false.

[ServiceContract]
[BindingRequirements(RequireOrderedDelivery=true)]
Public interface IMyContract
{
    [OperationContract]
    void DoSomething();
}

TransactionFlowRequirements: Transaction Flow Control

By default, bindings for a service contract are free to enable transaction flow or not. To specifically require or disallow transaction flow in a binding, use the TransactionFlowRequirements parameter of [BindingRequirements]. Possible values are Required, Disallowed, and Ignore. The default is Ignore.

[ServiceContract]
[BindingRequirements(TransactionFlowRequirements=RequirementsMode.Required)]
Public interface IMyContract
{
    [OperationContract]
    void DoSomething();
}

Defining Service Operations

There are three kinds of services:

  • Typed In a typed service, the parameters and return values of service operations are primitive or complex data types.
  • Untyped In an untyped service, the parameters and return values of service operations are messages.
  • Typed message In a typed message service, the parameters and return values of service operations are custom messages defined with message contracts.

Defining Typed Service Operations

A typed service operation is one that accepts simple or complex data as parameters and return values. Using a typed service operation feels very much like using a method of a class. The following service contract contains typed service operations.

[ServiceContract]
public interface IMeasurementConversion
{
    [OperationContract]
    float CalculateDistance(float x1, float y1, float x2, float y2);
    [OperationContract]
    int CalculateShippingZone(float x1, float y1);
}

Service operations can return values, as the preceding service operations do. But sometimes you might need to return more than one value or return a modified version of a parameter value. In these cases, you can use ref and out parameters, just as you would in object-oriented programming. In actuality, you are passing by value. The following service operations use ref and out parameters.

[ServiceContract]
public interface IMeasurementConversion
{
    [OperationContract]
    void MoveNorth(ref float x, ref float y, float distance);
    [OperationContract]
    void MoveSouth(ref float x, ref float y, float distance);
    [OperationContract]
    void MoveEast(ref float x, ref float y, float distance);
    [OperationContract]
    void MoveWest(ref float x, ref float y, float distance);
    [OperationContract]
    void CalculateDistance(float x1, float y1, float x2, float y2, out float 
       distance);
}

In addition to simple types and arrays, typed service operations can also accept complex data structures as parameters or return them as results. This requires the use of data contracts. See the earlier section titled "Understanding Data Contracts" and the later section titled "Programming Data Contracts" for more information.

Defining Untyped Service Operations

An untyped service operation accepts a Message parameter, which allows it to accept any kind of incoming message. The service operation code is responsible for interpreting the message. If the service operation sends a reply, it is also of type Message. The following service contract contains an untyped service operation.

[ServiceContract]
public interface IMyUntypedContract
{
    [OperationContract(IsOneWay=true, Action="*")] 
    void ProcessMessage(Message message);
}

Specifying the Action=?*? parameter in the [OperationContract] attribute causes the service operation to receive all incoming messages regardless of the service operation name originally specified by the client.

For information on how to create messages to send or interpret received messages using Message objects, see Chapter 7.

Defining Typed Message Service Operations

A typed message service operation accepts a custom message parameter—a message contract class. The service operation code is responsible for interpreting the message. If the service operation sends a reply, the reply is also a custom message, but it does not have to be the same type as the request message. The following code contains a service contract with a typed message service operation and a message contract defining a custom message.

[ServiceContract]
public interface IMyTypedMessageContract
{
    [OperationContract(IsOneWay=true, Action="*")] 
    void Register(Contact contact);
}

[MessageContract]
public class Contact
{
    [MessageHeader]
    string LastName;
    [MessageHeader]
    string FirstName;
    [MessageBody]
    string Phone;
    [MessageBody]
    string Email;
    [MessageBody]
    string Notes;
}

For information on how to create message contracts, see the section titled "Programming Message Contracts" later in this chapter. For information on how to implement typed message service operations, see Chapter 7.

Programming Data Contracts

You define a data contract by annotating a class with the [DataContract] attribute. Data contract members are indicated with [DataMember] attributes on the methods of the class.

[DataContract]
public class OrderItem
{
    [DataMember] public int Qty;
    [DataMember] public string PartNo;
    [DataMember] public string Description;
    [DataMember] public decimal Price; 
}

The [DataContract] Attribute

The [DataContract] attribute marks a class as a data contract.

[DataContract]
public class ICustomer
{
    ...
}

The following parameters can be specified for the [DataContract] attribute:

  • Namespace (Uri)
    Specifies the namespace for the data contract. By default, the namespace for a data contract is derived from the CLR namespace.
  • Name (string)
    Specifies the name for the data contract. By default, the name for a data contract is derived from the name of the class.

Name and Namespace: Naming a Data Contract

By default, a data contract takes its name and namespace from the class or structure the [DataContract] attribute is applied to. To set a different name, use the Name parameter of [DataContract]. To set a different namespace, use the Namespace parameter.

[DataContract(Namespace="http://tempuri.org", Name="Customer"]
class CustomerClass
{
    ...
}

The [DataMember] Attribute

The [DataMember] attribute marks the field, property, or event of a class as a member of a data contract.

[DataContract]
public class Customer
{
    [DataMember]
    public string CustomerID;
    [DataMember]
    public string LastName;
    [DataMember]
    public string FirstName;
    ...
}

The following parameters can be specified for the [DataMember] attribute:

  • IsOptional (boolean)
    Controls what happens if data that is part of the receiving-side data contract is not present. If IsOptional is false and the member is not present in the incoming data, an error results. If IsOptional is true, no error results. The default is false (member required).
  • MustUnderstand (boolean)
    Controls what happens if data is supplied that is not part of the receiving-side data contract. If MustUnderstand is true and the member is not part of the receiving side's data contract, an error results. If MustUnderstand is false, no error results. The default is false.
  • Name (string)
    Specifies the name for the data contract. By default, the name for a data contract is derived from the name of the class.
  • VersionAdded (int)
    Indicates the version of the data contract when the member was first added to the contract. The default is 1.
  • SerializeAs (SerializationReferenceMode)
    Controls what happens when two members of a data contract reference the same instance of an object. If SerializeAs is set to SerializationReferenceMode.ReferenceType, the result that is deserialized on the receiving side will likewise have two references to the same instance. If SerializeAs is set to SerializationReferenceMode.ValueType, reference semantics will be ignored and the receiving side will have references to two separate instances that have equal values. The default is SerializationReferenceMode.ReferenceType.

IsOptional: Requiring Data Members

To support versioning, data contract members can be optional or required. The IsOptional property controls what the receiving side does if a data member is not present. If IsOptional is false, an error occurs if the data member is missing in a message. The following code shows a data contract in which some members are required and some are optional.

[DataContract]
public class Contact
{
    [DataMember(IsOptional=false)]
    public string FirstName;
    [DataMember(IsOptional=true)]
    public string MiddleName;
    [DataMember(IsOptional=false)]
    public string LastName;
    [DataMember(IsOptional=false)]
    public string Address;
    [DataMember(IsOptional=true)]
    public string Address2;
}

Don't confuse IsOptional with MustUnderstand. Think of IsOptional as something the receiver specifies and MustUnderstand as something the sender specifies.

MustUnderstand: Requiring Data Members

A data contract can stipulate that the receiver must understand (expect) a data member. The MustUnderstand property controls what the receiving side does if a data member is present that is not part of its data contract. If MustUnderstand is true, an error occurs if the receiving side wasn't expecting the data member. The following code shows a data contract in which MustUnderstand is specified for some data members.

[DataContract]
public class Contact
{
    [DataMember(MustUnderstand=true)]
    public string FirstName;
    [DataMember(MustUnderstand=true)]
    public string MiddleName;
    [DataMember(MustUnderstand=true)]
    public string LastName;
    [DataMember(MustUnderstand=false)]
    public string Address;
    [DataMember(MustUnderstand=false)]
    public string Address2;
    [DataMember(MustUnderstand=true)]
    public string Phone;
}

Name: Naming a Data Member

By default, a data member takes its name from the field, property, or event the attribute is applied to. To set a different name, use the Name parameter of [DataMember]. The following data contract uses the Name parameter to specify several data member names.

[DataContract]
public class AirlineSeat
{
    [DataMember(Name="Column")]
    public char _column;
    [DataMember(Name="Row")]
    public int _row; 
    [DataMember(Name="Class")]
    public CabinClass _class; 
}

VersionAdded: Specifying a Version Number

To document the changes to a data contract, you can assign version numbers to data members. The default version number is 1. To specify a different version number, use the VersionAdded parameter and specify an integer version number. The following code shows a data contract that is now at version 3.

[DataContract]
public class AirlineSeat
{
    [DataMember]
    public char _column;
    [DataMember(VersionAdded=2)]
    public int _row; 
    [DataMember(VersionAdded=2)]
    public CabinClass _class; 
    [DataMember(VersionAdded=3)]
    public CabinClass _class; 
}

SerializeAs: Maintaining Reference Semantics

When a data contract class is serialized, more than one of its members might contain a reference to the same instance of an object. For example, a bill-to address and a ship-to address in a purchase order might reference the same address object. With the exception of strings, a data contract preserves CLR reference semantics, so the deserialized data contract will also have shared references. When you are communicating with another platform, this might be unwanted behavior. Use the SerializeAs property to enable or disable this behavior. Set SerializeAs to SerializationReferenceMode.ReferenceType to preserve reference semantics or set it to SerializationReferenceMode.ValueType to defeat reference semantics. The default is ReferenceType.

[DataContract]
public class AirlineSeat
{
    [DataMember(SerializeAs=SerializationReferenceMode.ValueType)]
    public Address BillToAddress;
    [DataMember(SerializeAs=SerializationReferenceMode.ValueType)]
    public Address ShipToAddress;
}

The [KnownType] Attribute

The serialization of data contracts usually contains all the information necessary for proper deserialization. But on occasion you might want to include more type information—for example, if you are using a dynamic array class such as Hashtable, which contains objects of other types. The definition of those other types won't be included in the serialization stream by default.

You use the [KnownType] attribute to make sure a type is included in the serialization stream. In the following code example, the MediaCatalog class contains a Hashtable. The types used with the Hashtable, DVD and CD, are identified to the data contract using [KnownType] attributes.

[DataContract]
public class DVD
{
    ...
}

[DataContract]
public class CD
{
    ...
}

[DataContract]
[KnownType(typeof(DVD))]
[KnownType(typeof(CD))]
public class MediaCatalog
{
    [DataMember]
    public Hashtable catalog;
}

[KnownType] in Service Operations

[KnownType] can be used in one other place: service contracts. You can specify [KnownType] attributes for a service operation when the type is not obvious. In the following code, the UploadFile service operation expects a MediaFile parameter. To permit the derived types MusicMediaFile and VideoMediaFile to also be used with the service operation, [KnownType] attributes are specified.

[ServiceContract]
public interface IMediaUploadService
{
    [OperationContract]
    [KnownType(typeof(MusicMediaFile))]
    [KnownType(typeof(VideoMediaFile))]
    void UploadFile(MediaFile file);
}

Programming Message Contracts

You define a message contract by annotating a class with the [MessageContract] attribute. The [MessageBody] and [MessageHeader] attributes are applied to class members.

[MessageContract]
public sealed class OrderItemRequest
{
    [MessageHeader]
    public string orderNo;
    [MessageBody]
    public OrderItem orderItem;
}

The [MessageContract] Attribute

The [MessageContract] attribute marks a class as a message contract.

[MessageContract]
public sealed class OrderItemRequest
{
    ...
}

The following parameters can be specified for the [MessageContract] attribute:

  • Action (string)
    Identifies the action part of the SOAP message. The default value is *, which means the message can target any service operation.
  • WrapperElementName (string)
    Specifies the name of the message body wrapper element. The wrapper element encloses the elements in the body of a message when the message is formatted in the document/wrapper style.
  • WrapperElementNamespace (string)
    Specifies the namespace of the message body wrapper element.

Action: Specifying an Action

The Action part of a SOAP message specifies the intended operation for the message. In typed messages, you can set the action to a specific string or to *, which means the message is suitable for any action. By default, a typed message has the action *.

[MessageContract(Action="http://SearchCatalog")]
class SearchCatalog
{
    ...
}

WrapperElementName and WrapperElementNamespace: Naming the Message Body Wrapper Element

The body of a message can be formatted in several styles. In the document/wrapper style, an outer XML element called the wrapper element surrounds the other elements in the body.

The WrapperElementName and WrapperElementNamespace properties allow you to set the name and namespace of the wrapper element, respectively. To set the name and namespace of the wrapper element, use the WrapperElementName and WrapperElementNamespace parameters of [MessageContract]. You specify the name and namespace as strings.

[MessageContract(WrapperElementName="Record", WrapperElementNamespace=
   "https://www.contoso.com/Record")]
public class RecordMessage
{
    ...
}

The [MessageBody] Attribute

The [MessageBody] attribute maps a message contract member to the body part of the message.

[MessageContract(Action="https://www.contoso.com/Contact")]
public class ContactMessage
{
    [MessageBody(Position=1)]
    public int Age;
    [MessageBody]
    public string Name;
}

The following parameters can be specified for the [MessageBody] attribute:

  • Name (string)
    Sets the name of the message contract member.
  • Namespace (string)
    Sets the namespace of the message contract member.
  • Position (int)
    Specifies where the message contract member appears in the body relative to other elements. This value influences only the order because more than one member can specify the same position number. The lowest possible position number is 0, which is the default value.
  • ReferenceMode (SerializationReferenceMode)
    Controls what happens when two members of a data contract reference the same instance of an object. If SerializeAs is set to SerializationReferenceMode.ReferenceType, the result that is deserialized on the receiving side will likewise have two references to the same instance. If SerializeAs is set to SerializationReferenceMode.ValueType, reference semantics will be ignored and the receiving side will have references to two separate instances that have equal values. The default is SerializationReferenceMode.ReferenceType.

The [MessageBodyArray] Attribute

When you map an array to the message body, you can use the [MessageBodyArray] attribute instead of [MessageBody] to emit optimal XML. [MessageBodyArray] is similar to [MessageBody] and accepts the same parameters (Name, Namespace, and Position). It can be applied only to arrays.

[MessageBodyArray]
public OrderItem[] items;

The [MessageHeader] Attribute

The [MessageHeader] attribute maps a message contract member to a SOAP message header.

[MessageContract(Action="https://www.contoso.com/Contact")]
public class ContactMessage
{
    [MessageHeader(Position=1)]
    public int Age;
    [MessageHeader]
    public string Name;
}

[MessageHeader] accepts the same parameters as [MessageBody]. See the section titled "The [MessageBody] Attribute" earlier in this chapter for a description of these parameters. [MessageHeader] also accepts these additional parameters:

  • Actor (string)
    Sets the Actor property of a SOAP header
  • MustUnderstand (boolean)
    Sets the MustUnderstand property of a SOAP header
  • Relay (boolean)
    Sets the Relay property of a SOAP header

Actor: Addressing an Endpoint

SOAP headers have an Actor property that indicates that the header is meant for a specific endpoint as the message makes its way from sender to receiver. The Actor parameter of [MessageHeader] sets the Actor property to a URI string.

[MessageContract]
public class MyMessage
{
    [MessageHeader(Actor="https://contoso.com/roleA", Relay="false")]
    public string HeaderA;
}

MustUnderstand: Making Headers Mandatory

SOAP headers have a MustUnderstand property. If the Actor property is set to true, the receiver indicated by the property must recognize the header in order to process the message. The MustUnderstand parameter of [MessageHeader] sets the MustUnderstand property.

[MessageContract]
public class MyMessage
{
    [MessageHeader(Actor="https://contoso.com/roleB", MustUnderstand="true")]
    public string HeaderB;
    ...
}

Relay: Message Relay to Downstream Endpoints

SOAP headers have a Relay property. If the Relay property is set to true, the endpoint indicated by the Actor property should pass the message to the next endpoint after processing the message. The Relay parameter of [MessageHeader] sets the Relay property.

[MessageContract]
public class MyMessage
{
    [MessageHeader(Actor="https://contoso.com/roleA", Relay="true")]
    public string HeaderA;
    [MessageHeader(Actor="https://contoso.com/roleB", Relay="false")]
    public string HeaderB;
}

Generating Client Code for Typed Messages

The Svcutil tool generates typed message classes by default. If your service uses typed messages, specify the /tm switch on the Svcutil command line to generate the appropriate typed message classes.

svcutil https://localhost:8000/MyService/ /tm

Programming Exercise: Contracts

In this programming exercise, we will create a service with service, data, and message contracts. The service is a parcel-shipping service. It implements two service contracts, allowing clients to access the service using typed service operations or typed messages. A data contract will define a contact structure. A message contract will define a typed message class. Both the service and client programs will be console applications.

This exercise has six development steps:

  1. Create the service program.
  2. Create the service configuration file.
  3. Build the service.
  4. Create the client.
  5. Create a configuration file for the client.
  6. Build the client.

Step 1: Create the Service Program

We'll create the service program and compile it to an EXE console program. Launch your development environment and create a new C# console application project named service. Enter the code in Listing 5-1.

To perform these tasks using Microsoft Visual Studio:

  1. Select New, Project from the File menu. Under Project Type, select Windows under Microsoft Visual C#. Under Templates, select Console Application. In the Name box, type service, in the Location box, type any path you want, and in the Solution Name box, type Contracts. Click OK to generate and open the new project.
  2. Replace the generated code in Program.cs with the code shown in Listing 5-1.

Your service project will need to reference System.Runtime.Serialization.dll and System.ServiceModel.dll.

To perform these tasks using Visual Studio:

  1. Right-click References in the Solution Explorer window, and then select Add Reference.
  2. In the Add Reference dialog box, on the .NET tab, select System.Runtime.Serialization.dll, and then click OK.
  3. Add another reference in the same manner for System.ServiceModel.dll.

Listing 5-1.

Service: Program.cs

using System;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Design;

namespace ProgrammingIndigo
{
    // Service contracts.

    [ServiceContract]
    public interface IParcelService
    {
        [OperationContract]
        void StartOrder();
        [OperationContract]
        void SetFrom(Contact contact);
        [OperationContract]
        void SetTo(Contact contact);
        [OperationContract]
        void SetWeight(int weight);
        [OperationContract]
        void FinishOrder();
    }

    [ServiceContract]
    public interface IParcelService2
    {
        [OperationContract]
        void PlaceOrder(OrderMessage orderMessage);
    }

    // Data contract.

    [DataContract]
    public class Contact
    {
        [DataMember]
        public string LastName;
        [DataMember]
        public string FirstName;
        [DataMember]
        public string Address;
        [DataMember]
        public string Address2;
        [DataMember]
        public string Phone;
    }

    // Message contract.

    [MessageContract]
    public class OrderMessage
    {
        [MessageBody]
        public Contact From;
        [MessageBody]
        public Contact To;
        [MessageBody]
        public int Weight;
    }
    
    // Service implementation class.

    [ServiceBehavior(InstanceMode=InstanceMode.PrivateSession)]
    public class ParcelService : IParcelService, IParcelService2
    {
        Contact from;
        Contact to;
        int weight;

        public void StartOrder()
        {
            Console.WriteLine("Start order");
        }

        public void SetFrom(Contact contact)
        {
            this.from = contact;
            Console.WriteLine("    From");
            Console.WriteLine("        " + contact.FirstName + " " + contact.LastName);
            Console.WriteLine("        " + contact.Address);
            Console.WriteLine("        " + contact.Phone);
        }

        public void SetTo(Contact contact)
        {
            this.to = contact;
            Console.WriteLine("    To");
            Console.WriteLine("        " + contact.FirstName + " " + contact.LastName);
            Console.WriteLine("        " + contact.Address);
            Console.WriteLine("        " + contact.Phone);
        }

        public void SetWeight(int weight)
        {
            this.weight = weight;
            Console.WriteLine("Weight {0}", weight);
        }

        public void FinishOrder()
        {
            Console.WriteLine("Finish order");
            Console.WriteLine();
        }

        public void PlaceOrder(OrderMessage orderMessage)
        {
            Console.WriteLine("PlaceOrder");
            ShowOrder(orderMessage);
        }

        void ShowOrder(OrderMessage order)
        {
            Console.WriteLine("Order contains:");
            Console.WriteLine("    From");
            Console.WriteLine("        " + order.From.FirstName + " " + 
               order.From.LastName);
            Console.WriteLine("        " + order.From.Address);
            Console.WriteLine("        " + order.From.Phone);
            Console.WriteLine("    To");
            Console.WriteLine("        " + order.To.FirstName + " " + 
               order.To.LastName);
            Console.WriteLine("        " + order.To.Address);
            Console.WriteLine("        " + order.To.Phone);
            Console.WriteLine("    Weight: " + order.Weight);
            Console.WriteLine();
        }

        // Host the service.
        // This service is a self-hosted EXE. 
        // This code would be unnecessary in a service hosted by IIS.

        public static void Main()
        {
            ServiceHost<ParcelService> serviceHost = new ServiceHost<ParcelService>();

            //Open the ServiceHost object.

            serviceHost.Open();

            // The service can now be accessed.

            Console.WriteLine("The service is ready");
            Console.WriteLine();
            Console.WriteLine("Press ENTER to shut down service.");
            Console.ReadLine();
            
            serviceHost.Close();
        }
    }
}

Step 2: Create the Service Configuration File

An App.config file is needed to specify endpoints and bindings for the service. Using an editor or development environment, create a text file with the code shown in Listing 5-2. Save the code under the name App.config in the same folder in which the service program is located.

To perform these tasks using Visual Studio:

  1. Right-click the service project, and then select Add, New Item. Select Application Configuration File, and then click Add. Name the file App.config.
  2. Enter the code in Listing 5-2 into App.config.

Listing 5-2.

Service: App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>

        <services>
            <service 
                serviceType="ProgrammingIndigo.ParcelService"
                behaviorConfiguration="ParcelServiceBehavior">
                <endpoint
                    address="https://localhost:8000/ParcelService/" 
                    bindingSectionName="wsProfileBinding" 
                    bindingConfiguration="ParcelServiceBinding" 
                    contractType="ProgrammingIndigo.IParcelService" />
                <endpoint
                    address="https://localhost:9000/ParcelService2/" 
                    bindingSectionName="wsProfileBinding" 
                    bindingConfiguration="ParcelServiceBinding" 
                    contractType="ProgrammingIndigo.IParcelService2" />
            </service>
        </services>

        <bindings>
            <wsProfileBinding>
                    <binding configurationName="ParcelServiceBinding"
                             reliableSessionEnabled="true"/>
            </wsProfileBinding>
        </bindings>

        <behaviors>
            <behavior 
                configurationName="ParcelServiceBehavior"
                returnUnknownExceptionsAsFaults="true" >
            </behavior>
            <behavior 
                configurationName="ParcelServiceBehavior2"
                returnUnknownExceptionsAsFaults="true" >
            </behavior>
        </behaviors>
    
    </system.serviceModel>
</configuration>

Step 3: Build the Service

Build the service program to make Service.exe. Resolve any typographical errors.

To perform the task using Visual Studio, select Build Solution from the Build menu to generate Service.exe.

The service is now ready. Next we need a client program to access it.

Step 4: Create the Client

To create the client program, launch your development environment and create a new C# console application project named client. Enter the code in Listing 5-3.

To perform these tasks using Visual Studio:

  1. Select New, Project from the File menu. Under Project Type, select Windows under Visual C#. Under Templates, select Console Application. In the Name box, type client, in the Location box, type any path you want, and in the Solution Name box, type Contracts. Click OK to generate and open the new project.
  2. Replace the generated code in Program.cs with the code shown in Listing 5-3.

Your client project will need to reference System.Runtime.Serialization.dll and System.ServiceModel.dll.

To perform these tasks using Visual Studio:

  1. Right-click References in the Solution Explorer window, and then select Add Reference.
  2. In the Add Reference dialog box, on the .NET tab, select System.Runtime.Serialization.dll, and then click OK.
  3. Add another reference in the same manner for System.ServiceModel.dll.

Build the client to create Client.exe.

Listing 5-3.

Client: Program.cs

using System;
using System.Runtime.Serialization;
using System.ServiceModel;

namespace ProgrammingIndigo
{
    // Service contract.

    [ServiceContract]
    public interface IParcelService
    {
        [OperationContract]
        void StartOrder();
        [OperationContract]
        void SetFrom(Contact contact);
        [OperationContract]
        void SetTo(Contact contact);
        [OperationContract]
        void SetWeight(int weight);
        [OperationContract]
        void FinishOrder();
    }

    [ServiceContract]
    public interface IParcelService2
    {
        [OperationContract]
        void PlaceOrder(OrderMessage orderMessage);
    }

    [DataContract]
    public class Contact
    {
        [DataMember]
        public string LastName;
        [DataMember]
        public string FirstName;
        [DataMember]
        public string Address;
        [DataMember]
        public string Address2;
        [DataMember]
        public string Phone;
    }

    [MessageContract]
    public class OrderMessage
    {
        [MessageBody]
        public Contact From;
        [MessageBody]
        public Contact To;
        [MessageBody]
        public int Weight;
    }

    //Client implementation code.

    class Client
    {
        static void Main()
        {
            Contact from = new Contact();
            from.FirstName = "Kim";
            from.LastName = "Akers";
            from.Address = "100 Main St";
            from.Address2 = "Redmond, WA 98052";
            from.Phone = "555-555-1001";

            Contact to = new Contact();
            to.FirstName = "Jeff";
            to.LastName = "Smith";
            to.Address = "1201 Flower St";
            to.Address2 = "New York, NY 98052";
            to.Phone = "555-555-1002";

            // Create a proxy.
            IParcelService proxy = C
               hannelFactory.CreateChannel<IParcelService>("ParcelServiceEndpoint");

            Console.WriteLine("Placing order 1 with ParcelService");
            Console.WriteLine("    Starting order");
            proxy.StartOrder();

            Console.WriteLine("    Setting From address");
            proxy.SetFrom(from);

            Console.WriteLine("    Setting To address");
            proxy.SetTo(to);

            Console.WriteLine("    Setting Weight");
            proxy.SetWeight(15);

            Console.WriteLine("    Finishing order");
            proxy.FinishOrder();

            Console.WriteLine("Order 1 is complete");
            Console.WriteLine();
            ((IChannel)proxy).Close();

            // Create a second proxy.
            IParcelService2 proxy2 = 
               ChannelFactory.CreateChannel<IParcelService2>("ParcelService2Endpoint");
            
            Console.WriteLine("Placing order 2 with ParcelService2");

            Console.WriteLine("    Placing order");
            OrderMessage order = new OrderMessage();
            order.To = to;
            order.From = from;
            order.Weight = 25;
            proxy2.PlaceOrder(order);

            Console.WriteLine("Order 2 is complete");
            Console.WriteLine();
            ((IChannel)proxy2).Close();

            Console.WriteLine();
            Console.WriteLine("Press ENTER to shut down client");
            Console.ReadLine();
        }
    }
}

Step 5: Create a Configuration File for the Client

A Client.exe.config file is needed to specify the service endpoint and binding to use. Using an editor or development environment, create a text file with the code shown in Listing 5-4. Save the code under the name Client.exe.config.

To perform these tasks using Visual Studio:

  1. Right-click the service project, and then select Add, New Item. Select Application Configuration File, and then click Add. Name the file App.config. (It will be copied to Client.exe.config at build time.)
  2. Enter the code in Listing 5-4 into App.config.

Listing 5-4.

Client: App.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <client>
            <endpoint
                configurationName="ParcelServiceEndpoint"
                address="https://localhost:8000/ParcelService/" 
                bindingSectionName="wsProfileBinding" 
                bindingConfiguration="ParcelServiceBinding" 
                contractType="ProgrammingIndigo.IParcelService" />
            <endpoint
                configurationName="ParcelService2Endpoint"
                address="https://localhost:9000/ParcelService2/" 
                bindingSectionName="wsProfileBinding" 
                bindingConfiguration="ParcelServiceBinding" 
                contractType="ProgrammingIndigo.IParcelService2" />
        </client>

        <bindings>
            <wsProfileBinding>
                <binding configurationName="ParcelServiceBinding"
                         reliableSessionEnabled="true"/>
            </wsProfileBinding>
        </bindings>
    </system.serviceModel>
</configuration>

Step 6: Build the Client

Build the client program to make Client.exe. Resolve any typographical errors.

To peform this task using Visual Studio, select Build Solution from the Build menu to generate Client.exe.

Deployment

We're now ready to try things out. Run the service from your development environment or from a command line. You should see output for the service like that shown in Figure 5-2. If the program fails, double-check that you've properly carried out each of the preceding steps.

Aa480202.progindigoch5_02(en-us,MSDN.10).gif

Figure 5-2. Contracts service

Press ENTER on the client to shut it down, and then do the same for the service. Congratulations on successfully completing a service with multiple endpoints!

The first order sent by the client accesses the typed service operations of the IParcelService contract. The second order sent by the client accesses the typed message of the IParcelService2 contract.

If you want to run the service and the client on separate machines, replace localhost with the fully qualified domain name of the server machine in the client and service .config files.

Understanding the Service Code

The service is self-hosted, so it must be explicitly launched just like the client. The service program code is shown in Listing 5-1. The code in the static Main function hosts the service. A ServiceHost object is created for the ParcelService class and opened. Two endpoints for the service are defined in configuration settings, one for the IParcelService contract and one for the IParcelService2 contract. The service configuration file is shown in Listing 5-2.

// Host the service.
// This service is a self-hosted EXE. 
// This code would be unnecessary in a service hosted by IIS.

public static void Main()
{
    ServiceHost<ParcelService> serviceHost = new ServiceHost<ParcelService>();

    // Open the ServiceHost object.

    serviceHost.Open();

    // The service can now be accessed.

    Console.WriteLine("The service is ready");
    Console.WriteLine();
    Console.WriteLine("Press ENTER to shut down service.");
    Console.ReadLine();
            
    serviceHost.Close();
}

This service contains two service contracts. The service contract IParcelService is a typed service that uses sessions. A client must call a series of operations to place a shipping order with this contract. The second service contract, IParcelService2, is a typed message service. A client can place a shipping order by sending a single message with this contract.

// Service contracts.

[ServiceContract]
public interface IParcelService
{
    [OperationContract]
    void StartOrder();
    [OperationContract]
    void SetFrom(Contact contact);
    [OperationContract]
    void SetTo(Contact contact);
    [OperationContract]
    void SetWeight(int weight);
    [OperationContract]
    void FinishOrder();
}

[ServiceContract]
public interface IParcelService2
{
    [OperationContract]
    void PlaceOrder(OrderMessage orderMessage);
}

Some of the service operations in IParcelService accept a Contact structure. A data contract is defined for Contact.

// Data contract.

[DataContract]
public class Contact
{
    [DataMember]
    public string LastName;
    [DataMember]
    public string FirstName;
    [DataMember]
    public string Address;
    [DataMember]
    public string Address2;
    [DataMember]
    public string Phone;
}

The IParcelService2 typed message service contract accepts a typed message named OrderMessage. A message contract is defined for OrderMessage.

// Message contract.

[MessageContract]
public class OrderMessage
{
    [MessageBody]
    public Contact From;
    [MessageBody]
    public Contact To;
    [MessageBody]
    public int Weight;
}

The ParcelService class implements the service operations for both the IParcelService and IParcelService2 service contracts. The service operations echo their activity in the service console window.

// Service implementation class.

[ServiceBehavior(InstanceMode=InstanceMode.PrivateSession)]
public class ParcelService : IParcelService, IParcelService2
{
    ...
    public void SetFrom(Contact contact)
    {
        this.from = contact;
        Console.WriteLine("    From");
        Console.WriteLine("        " + contact.FirstName + " " + contact.LastName);
        Console.WriteLine("        " + contact.Address);
        Console.WriteLine("        " + contact.Phone);
    }
    ...
}

The typed message service operation, PlaceOrder, receives an OrderMessage. The client displays the order contents by calling a ShowOrder method. The typed message can be treated as a message and as an object.

public void PlaceOrder(OrderMessage orderMessage)
{
    Console.WriteLine("PlaceOrder");
    ShowOrder(orderMessage);
}

void ShowOrder(OrderMessage order)
{
    Console.WriteLine("Order contains:");
    Console.WriteLine("    From");
    Console.WriteLine("        " + order.From.FirstName + " " + order.From.LastName);
    Console.WriteLine("        " + order.From.Address);
    Console.WriteLine("        " + order.From.Phone);
    Console.WriteLine("    To");
    Console.WriteLine("        " + order.To.FirstName + " " + order.To.LastName);
    Console.WriteLine("        " + order.To.Address);
    Console.WriteLine("        " + order.To.Phone);
    Console.WriteLine("    Weight: " + order.Weight);
    Console.WriteLine();
}

Understanding the Client Code

The client contains the same contract definitions described earlier for the service. In this programming exercise, generated client code was not used, so there is no client proxy class. The client will use the ChannelFactory class to create proxy channels.

The client creates its first proxy to the service endpoint that implements the IParcelService contract. In the client configuration file, this endpoint is defined under the name ParcelServiceEndpoint. Once the proxy is created, the client calls a series of service operations to submit a shipping order. After the order has been sent, the channel proxy is closed.

// Create a proxy.
IParcelService proxy = 
   ChannelFactory.CreateChannel<IParcelService>("ParcelServiceEndpoint");

Console.WriteLine("Placing order 1 with ParcelService");
Console.WriteLine("    Starting order");
proxy.StartOrder();

Console.WriteLine("    Setting From address");
proxy.SetFrom(from);

Console.WriteLine("    Setting To address");
proxy.SetTo(to);

Console.WriteLine("    Setting Weight");
proxy.SetWeight(15);

Console.WriteLine("    Finishing order");
proxy.FinishOrder();

Console.WriteLine("Order 1 is complete");
Console.WriteLine();
((IChannel)proxy).Close();

Next the client creates a second proxy to the service endpoint that implements the IParcelService2 contract. In the client configuration file, this endpoint is defined under the name ParcelService2Endpoint. Once the proxy is created, the client creates an OrderMessage typed message and submits an order in a single service operation. After the order has been sent, the channel proxy is closed.

// Create a second proxy.
IParcelService2 proxy2 = 
   ChannelFactory.CreateChannel<IParcelService2>("ParcelService2Endpoint");
            
Console.WriteLine("Placing order 2 with ParcelService2");

Console.WriteLine("    Placing order");
OrderMessage order = new OrderMessage();
order.To = to;
order.From = from;
order.Weight = 25;
proxy2.PlaceOrder(order);

Console.WriteLine("Order 2 is complete");
Console.WriteLine();
((IChannel)proxy2).Close();

If communicating cross-machine, the references to localhost in the client and service configuration files must be replaced by the fully qualified domain name of the server.

Summary

This chapter described in detail the three types of contracts in Indigo: service contracts, data contracts, and message contracts. Contracts make interoperable, loosely coupled service orientation possible.

Service contracts describe the operations a service can perform and are behavioral in nature. A service contract is defined with the [ServiceContract] and [OperationContract] attributes. Binding requirements can be specified for the contract with a [BindingRequirements] attribute.

Data contracts describe data structures. A data contract is defined primarily with the [DataContract] and [DataMember] attributes.

Message contracts describe SOAP messages. A message contract is defined primarily with the [MessageContract], [MessageBody], and [MessageHeader] attributes.

The programming tasks in the chapter covered how to define service, data, and message contracts. The attributes used for defining contracts and their parameters were described.

The programming exercise developed a parcel shipping service with multiple service contracts, a data contract, and a message contract. The client was able to place orders by using a typed service and a typed message service.

In the next chapter, we'll cover clients.

Excerpted from the upcoming book, Programming Indigo by David Pallmann.