The concept of a service contract is central to Indigo. In fact, its probably the single most important concept in terms of designing Indigo-based systems.
Service contracts express valid message exchange patterns between two parties. The party that initiates communication is called the initiator. The other party is called the service.
Service contracts specify one or more operation contracts. An operation contract represents an individual message exchange or a correlated request/reply message exchange.
Indigo allows service contracts to be expressed either in an XML format (specifically WSDL/1.1) or as annotated CLR interfaces. In reality, neither of these formats is the "native" contract format. Rather, we shred both formats into a simple in-memory tree structure that captures the "essence" of the contract. To do "contract-first" development in Indigo, you can define your contracts in WSDL, in annotated CLR interfaces, or any domain-specific language you chose provided you have a way to turn that DSL into either WSDL or a CLR assembly.
Here's the simplest possible Indigo service contract expressed in C#:
[ServiceContract]
interface ContractOne
{
[OperationContract(IsOneWay = true, Action="*")]
void ProcessMessage(Message msg); // System.ServiceModel.Message is a built-in type in Indigo that means "untyped message"
}
This contract describes a simple message exchange pattern (MEP) in which the initiator may send any message it likes to the service. The messages are "one-way" in that there is no correlated reply. Moreover, the initiator will get control back from Indigo prior to the message being dispatched at the service, so this is truly asynchronous delivery with respect to the message receiver.
To allow a given contract to distinguish between different kinds of messages, we treat the Action URI as a first class property of a message and allow each operation contract to indicate a specific URI used to match messages. The ActionURI is the true external "name" of the operation as far as the contract is concerned. The choice of the local method name just that - a local choice that doesn't effect the external contract. For example, consider this contract:
[ServiceContract]
interface ContractTwo
{
[OperationContract(IsOneWay = true, Action="urn:crud:insert")]
void ProcessInsertMessage(Message msg);
[OperationContract(IsOneWay = true, Action="urn:crud:update")]
void ProcessUpdateMessage(Message msg);
[OperationContract(IsOneWay = true, Action="urn:crud:delete")]
void ProcessDeleteMessage(Message msg);
[OperationContract(IsOneWay = true, Action="*")]
void ProcessUnrecognizedMessage(Message msg);
}
This contract expresses the simple "switch statement" in which there are three known message kinds (insert/update/delete) that will be used to route or dispatch the message appropriately. Here's an example of how a service would take advantage of this:
class ServiceTwo : ContractTwo
{
public void ProcessInsertMessage(Message msg) { /* do insert logic */ }
public void ProcessUpdateMessage(Message msg) { /* do update logic */ }
public void ProcessDeleteMessage(Message msg) { /* do delete logic */ }
public void ProcessUnrecognizedMessage(Message msg) { /* do "default" logic for unrecognized messages */ }
}
Here's what's interesting about the way contracts work.
The initiator (commonly called the "client") and the service (commonly called the "server") by definition have distinct definitions of the contract used to communicate. With autonomous services, it's not practical to enforce CLR/JVM-style type unification on an Internet scale. For that reason, Indigo assumes that there is no centralized, authoritative contract definition. Rather, a contract in Indigo is effectively a test one can perform on a given set of messages. Each party has their own "test" and provided the actual messages being exchanged satisfy both party's tests, there are no contract violations and all is well.
By way of example, consider a service that implemented ContractOne:
class ServiceOne : ContractOne
{
public void ProcessMessage(Message msg) { /* do "default" logic for all messages */ }
}
Because all MEPs that adhere to ContractTwo also adhere to ContractOne, we can use a channel of type ContractTwo to talk to ServiceOne:
ContractTwo channel = ChannelFactory.CreateChannel<ContractTwo>(addressOfServiceOne);
channel.ProcessInsertMessage(CreateAnInsertMessage());
In this example, the insert message will be "routed" to the ServiceOne.ProcessMessage method body for processing. Similarly, if we used a ContractOne channel to talk to ServiceTwo, messages whose action URI matched the insert, update, or delete operations would be routed appropriately to the service's Process[Insert|Update|Delete]Message operations. All other messages would be routed to the ProcessUnrecognizedMessage method body.
Most (but not all) services that go to the trouble of specifying action URIs also have some expectation on the message content. The two contracts we've looked at so far use System.ServiceModel.Message, which is the "untyped" message that makes no expectations about content other than the SOAP Envelope/Header/Body construct.
To allow messages to be easily "schematized", we can define a typed message using Indigo's [MessageContract] attribute as follows:
[MessageContract]
public class DeleteMessage {
[MessageHeader] public int UserID;
[MessageHeader] public bool NotifyOtherUsers;
[MessageBody] public string Justification;
}
[MessageContract]
public class UserDataMessage {
[MessageHeader] public int UserID;
[MessageBody] public string Name;
[MessageBody] public DateTime DateOfBirth;
}
The [MessageHeader] attribute indicates that the field or property appears as a SOAP header block. The [MessageBody] attribute indicates that the field or property appears in the SOAP body. For example, the DeleteMessage above would yield the following SOAP message:
<S:Envelope>
<S:Header>
<ns:UserID>4252</ns:UserID>
<ns:NotifyOtherUsers>true</ns:NotifyOtherUsers>
</S:Header>
<S:Body>
<ns:Justification>This user is a troll.</ns:Justification>
</S:Body>
</S:Envelope>
In order to define a service contract that uses these types, we simply change the parameter type from Message to our typed message types:
[ServiceContract]
interface ContractThree
{
[OperationContract(IsOneWay = true, Action="urn:crud:insert", Style=ServiceOperationStyle.DocumentBare)]
void ProcessInsertMessage(UserDataMessage msg);
[OperationContract(IsOneWay = true, Action="urn:crud:update", Style=ServiceOperationStyle.DocumentBare)]
void ProcessUpdateMessage(UserDataMessage msg);
[OperationContract(IsOneWay = true, Action="urn:crud:delete", Style=ServiceOperationStyle.DocumentBare)]
void ProcessDeleteMessage(DeleteMessage msg);
[OperationContract(IsOneWay = true, Action="*")]
void ProcessUnrecognizedMessage(Message msg);
}
To use this contract with a channel, we'd write the following code:
ContractThree channel = ChannelFactory.CreateChannel<ContractThree>(addressOfServiceOne);
UserDataMessage msg = new UserDataMessage();
msg.UserID = 4252;
msg.NotifyOtherUsers = true;
msg.Justification = "This user is a troll."
channel.ProcessInsertMessage(msg);
This "typed message" style of contract is actually the closest to the "native" contract system of Indigo. It's also the closest to the WSDL portType model.
As a convenience, Indigo allows you to declare an "anonymous" message type using the parameters of a given [OperationContract] method declaration:
[ServiceContract]
interface ContractFour
{
[OperationContract(IsOneWay = true, Action="urn:crud:insert", Style=ServiceOperationStyle.DocumentBare)]
void ProcessInsertMessage(
[MessageHeader] int UserID,
[MessageBody] string Name,
[MessageBody] DateTime DateOfBirth
);
[OperationContract(IsOneWay = true, Action="urn:crud:update", Style=ServiceOperationStyle.DocumentBare)]
void ProcessUpdateMessage(
[MessageHeader] int UserID,
[MessageBody] string Name,
[MessageBody] DateTime DateOfBirth
);
[OperationContract(IsOneWay = true, Action="urn:crud:delete", Style=ServiceOperationStyle.DocumentBare)]
void ProcessDeleteMessage(
[MessageHeader] int UserID,
[MessageHeader] bool NotifyOtherUsers,
[MessageBody] string Justification
);
[OperationContract(IsOneWay = true, Action="*")]
void ProcessUnrecognizedMessage(Message msg);
}
As far as the external contract is concerned, ContractThree and ContractFour are identical. The only differences are "local" to the in-memory programming model. If I use WS-MetadataExchange to ask your service for its contract, I can't tell the difference between ContractThree or ContractFour.
The upside of defining an anonymous message type using parameters is that the sender side is slightly smaller:
ContractFour channel = ChannelFactory.CreateChannel<ContractFour>(addressOfServiceOne);
channel.ProcessInsertMessage(4252, true, "This user is a troll.");
The downside is that the receiver doesn't have anything that captures all of the pieces of the message as a single unit. Of course, because the external contract is identical, I could use ContractFour on the initiator side and use ContractThree on the service-side's implementation.
Now for the quiz.
Can I use ContractThree or ContractFour channels to talk to ServiceOne? As far as Indigo is concerned, yes. Indigo will happily dispatch any message to ServiceOne.ProcessMessage.
Can I use ContractThree or ContractFour channels to talk to ServiceTwo? As far as Indigo is concerned, yes. The Process[Insert|Update|Delete]Message operations will receive messages that adhere to our typed message schemas. As long as that's what the service implementation actually expected, we're golden.
The interesting question is what happens if I use ContractOne or ContractTwo channels to talk to a service that implements ContractThree or ContractFour?
For messages whose action URI doesn't match the three well-known Action URI, it's fine. It gets routed to the ProcessUnrecognizedMessage method body just like before.
If, however, the Action URI matches either of the three well-known URI, Indigo will attempt to validate the message and shred it into the appropriate fields or parameters. If the validation fails due to either a type mismatch (e.g., if the UserID element contains the string "fred") or due to MustUnderstand headers not being mapped, then Indigo will trap the error and not dispatch to your method.
Where are we?
We just looked how individual messages get expressed in Indigo contracts.
An indigo service contract describes a message exchange between two parties.
Indigo service contracts bind ActionURIs to the content of a message sent by each party.
Binding an action URI to System.ServiceModel.Message puts no restriction on message content as far as Indigo is concerned. It's up to the apps to sort things out.
Using [MessageContract] or typed method parameters constrains the content of messages. Indigo will not only enforce these constraints, but it will also conveniently shred the indicated message parts into strongly typed fields, properties, or parameters.
Next time, we'll look at synchronous and asynchronous request/reply message exchange patterns.
Posted
Feb 12 2005, 08:18 PM
by
don-box