I hope that by this point in the chapter you have a clear conceptual view of service orientation. For the next few pages, let’s look at how this concept can physically take shape in WCF applications. In our example, we will be building a simple order processing service that receives customer orders. To keep things simple, there are two message participants, as shown in Figure 2-3.
Figure 2-3: A simple message exchange
The purpose of these code samples is to solidify your vision of service orientation and provide an introduction to WCF, not to detail every aspect of WCF or to build a fully functional order processing system. The types and mechanisms introduced in these examples will be detailed throughout this book.
Typically, the place to start in a service-oriented application is to create the contract. To keep our example simple, an order will contain a product ID, a quantity, and a status message. Given these three fields, an order could be represented with the following pseudo-schema:
<Order> <ProdID>xs:integer</ProdID> <Qty>xs:integer</Qty> <Status>xs:string</Status> </Order>
From our message anatomy and addressing discussions, we know that messages need more addressing structure if they are going to use WS-Addressing. In our order processing service, both the sender and the receiver agree to use SOAP messages that adhere to the WS-Address-ing specification to dictate the structure of the message. Given these rules, the following is an example of a properly structured message:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsa="http:// schemas.xmlsoap.org/ws/2004/08/addressing"> <s:Header> <wsa:Action s:mustUnderstand="1">urn:SubmitOrder</wsa:Action> <wsa:MessageID>4</wsa:MessageID> <wsa:ReplyTo> <wsa:Address> http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous </wsa:Address> </wsa:ReplyTo> <wsa:To s:mustUnderstand="1">http://localhost:8000/Order</wsa:To> </s:Header> <s:Body> <Order> <ProdID>6</ProdID> <Qty>6</Qty> <Status>order placed</Status> </Order> </s:Body> </s:Envelope>
After we have created the schemas that describe our messages, our next step is to define the endpoint that will receive those messages. For this, we can turn to WSDL. You might be thinking to yourself: “I am not really in the mood to deal with raw schemas or WSDL.” Well, you are not alone. The WCF team has provided a way for us to express a contract (both the schema and the WSDL) in the Microsoft .NET Framework language of our choosing (in this book, it will be C#). Basically, the expression of a contract in C# can be turned into XSD-based and WSDL-based contracts on demand.
When choosing to express our contracts in C#, we can choose to define a class or an interface. An example of a contract defined as an interface in C# is shown here:
// file: Contracts.cs using System; using System.ServiceModel; using System.ServiceModel.Channels; // define the contract for the service [ServiceContract(Namespace = "http://wintellect.com/ProcessOrder")] public interface IProcessOrder { [OperationContract(Action="urn:SubmitOrder")] void SubmitOrder(Message order); }
Notice the ServiceContractAttribute and OperationContractAttribute annotations. We will talk more about these attributes in Chapter 9, “Contracts.” For now, it is enough to know that this interface is distinguished from other .NET Framework interfaces through the addition of these custom attributes. Also notice the signature of the SubmitOrder interface method. The only parameter in this method is of type System.ServiceModel.Message. This parameter represents any message that can be sent to a service from an initial sender or intermediary. The Message type is a very interesting and somewhat complex type that will be discussed thoroughly in Chapter 5, “Messages,” but for now, assume that the message sent by the initial sender can be represented by the System.ServiceModel.Message type.
Regardless of the way we choose to express our contracts, it should be agreed upon and shared before further work is done on either the sender or the receiver applications. In practice, the receiver defines the required message structure contract, and the sender normally attempts to build and send messages that adhere to this contract.
There is nothing preventing the sender from sending messages that do not adhere to the contract defined by the receiver. For this reason, the receiver’s first task should be to validate received messages against the contract. This approach helps ensure that the receiver’s data structures do not become corrupted. These points are frequently debated in distributed development communities, so there are other opinions on this matter.
This contract can now be compiled into an assembly. Once the compilation is complete, the assembly can be distributed to the sender and the receiver. This assembly represents the contract between the sender and the receiver. While there will certainly be times when the contract will change, we should consider the contract immutable after it has been shared. We will discuss contract versioning in Chapter 9.
Now that we have our contract in place, let’s build the receiver application. The first order of business is to build a class that implements the interface defined in our contract:
// File: Receiver.cs // Implement the interface defined in the contract assembly public sealed class MyService : IProcessOrder { public void SubmitOrder(Message order) { // Do work here } }
Because this is a simple application, we are content to print text to the console and write the inbound message to a file:
// File: Receiver.cs using System; using System.Xml; using System.IO; using System.ServiceModel; using System.ServiceModel.Channels; // Implement the interface defined in the contract assembly public sealed class MyService : IProcessOrder { public void SubmitOrder(Message order) { // Create a file name from the MessageID String fileName = "Order" + order.Headers.MessageId.ToString() + ".xml"; // Signal that a message has arrived Console.WriteLine("Message ID {0} received", order.Headers.MessageId.ToString()); // create an XmlDictionaryWriter to write to a file XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter( new FileStream(fileName, FileMode.Create)); // write the message to a file order.WriteMessage(writer); writer.Close(); } }
Our next task is to allow the MyService type to receive inbound messages. To receive a message:
MyService must be loaded into an AppDomain.
MyService (or another type) must be listening for inbound messages.
An instance of this type must be created at the appropriate time and referenced as long as it is needed (to prevent the garbage collector from releasing the object’s memory).
When a message arrives, it must be dispatched to a MyService instance and the SubmitOrder method invoked.
These tasks are commonly performed via a host. We will talk more about hosts in Chapter 10, but for now, assume that our AppDomain is hosted in a console application and the type responsible for managing the lifetime of and dispatching messages to MyService objects is the System.ServiceModel.ServiceHost type. Our console application is shown here:
// File: ReceiverHost.cs using System; using System.Xml; using System.ServiceModel; internal static class ReceiverHost { public static void Main() { // Define the binding for the service WSHttpBinding binding = new WSHttpBinding(SecurityMode.None); // Use the text encoder binding.MessageEncoding = WSMessageEncoding.Text; // Define the address for the service Uri addressURI = new Uri(@"http://localhost:4000/Order"); // Instantiate a Service host using the MyService type ServiceHost svc = new ServiceHost(typeof(MyService)); // Add an endpoint to the service with the // contract, binding, and address svc.AddServiceEndpoint(typeof(IProcessOrder), binding, addressURI); // Open the service host to start listening svc.Open(); Console.WriteLine("The receiver is ready"); Console.ReadLine(); svc.Close(); } }
In our console application, we must set some properties of the service before we can host it. As you will see in subsequent chapters, every service contains an address, a binding, and a contract. These mechanisms are often called the ABCs of WCF. For now, assume the following:
An address describes where the service will be listening for inbound messages.
A binding describes how the service will be listening for messages.
A contract describes what sorts of messages the service will receive.
In our example, we are using the WSHttpBinding binding to define how the service will listen for inbound messages. We’ll talk more about bindings in Chapter 8. Our service also uses the Uri type to define the address our service will be listening on. Our service then instantiates a ServiceHost object that uses our MyService class to provide shape to the ServiceHost. ServiceHosts do not have default endpoints, so we must add our own by calling the AddServiceEndpoint instance method. It is at this point that our console application is ready to start listening at the address http://localhost:8000/Order for inbound messages. A call to the Open instance method begins the listening loop (among other things).
You might be wondering what happens when a message arrives at http://localhost:8000/Order. The answer depends on what sort of message arrives at the endpoint. For that, let’s switch gears and build our simple message sending console application. At a high level, our message sender is going to have to know the following:
Where the service is located (the address)
How the service expects messages to be sent (the service binding)
What types of messages the service expects (the contract)
Assuming that these facts are known, the following is a reasonable message sending application:
// File: Sender.cs using System; using System.Text; using System.Xml; using System.ServiceModel; using System.Runtime.Serialization; using System.IO; using System.ServiceModel.Channels; public static class Sender { public static void Main(){ Console.WriteLine("Press ENTER when the receiver is ready"); Console.ReadLine(); // address of the receiving application EndpointAddress address = new EndpointAddress(@"http://localhost:4000/Order"); // Define how we will communicate with the service // In this case, use the WS-* compliant HTTP binding WSHttpBinding binding = new WSHttpBinding(SecurityMode.None); binding.MessageEncoding = WSMessageEncoding.Text; // Create a channel ChannelFactory<IProcessOrder> channel = new ChannelFactory<IProcessOrder>(binding, address); // Use the channel factory to create a proxy IProcessOrder proxy = channel.CreateChannel(); // Create some messages Message msg = null; for (Int32 i = 0; i < 10; i++) { // Call our helper method to create the message // notice the use of the Action defined in // the IProcessOrder contract... msg = GenerateMessage(i,i); // Give the message a MessageID SOAP header UniqueId uniqueId = new UniqueId(i.ToString()); msg.Headers.MessageId = uniqueId; Console.WriteLine("Sending Message # {0}", uniqueId.ToString()); // Give the message an Action SOAP header msg.Headers.Action = "urn:SubmitOrder"; // Send the message proxy.SubmitOrder(msg); } } // method for creating a Message private static Message GenerateMessage(Int32 productID, Int32 qty) { MemoryStream stream = new MemoryStream(); XmlDictionaryWriter writer = XmlDictionaryWriter.CreateTextWriter( stream, Encoding.UTF8, false); writer.WriteStartElement("Order"); writer.WriteElementString("ProdID", productID.ToString()); writer.WriteElementString("Qty", qty.ToString()); writer.WriteEndElement(); writer.Flush(); stream.Position = 0; XmlDictionaryReader reader = XmlDictionaryReader.CreateTextReader( stream, XmlDictionaryReaderQuotas.Max); // Create the message with the Action and the body return Message.CreateMessage(MessageVersion.Soap12WSAddressing10, String.Empty, reader); } }
Try not to get too distracted by the ChannelFactory type just yet-we will fully explore this type in Chapter 4. For now, notice the code in the for loop. The instructions in the loop generate 10 messages and assign each one a pseudo-unique ID and an action.
At this point, we should have two executables (ReceiverHost.exe and Sender.exe) representing an ultimate receiver and an initial sender. If we run both console applications, wait for the receiver to initialize, and press ENTER on the initial sender application, we should see the following on the receiver:
The receiver is ready Message ID 0 received Message ID 1 received Message ID 2 received Message ID 3 received Message ID 4 received Message ID 5 received Message ID 6 received Message ID 7 received Message ID 8 received Message ID 9 received
Congratulations! You have just written a service-oriented application with WCF. Remember that the service is writing inbound messages to a file. If we examine one of the files that our service wrote, we see the following:
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"> <s:Header> <a:Action s:mustUnderstand="1">urn:SubmitOrder</a:Action> <a:MessageID>1</a:MessageID> <a:ReplyTo> <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address> </a:ReplyTo> <a:To s:mustUnderstand="1">http://localhost:4000/Order</a:To> </s:Header> <s:Body> <Order> <ProdID>1</ProdID> <Qty>1</Qty> </Order> </s:Body> </s:Envelope>
The headers in this message should look eerily similar to the ones we see in the WS-Addressing specification, and their values should look like the properties we set in our message sending application. In fact, the System.ServiceModel.Message type exposes a property named Headers that is of type System.ServiceModel.MessageHeaders. This MessageHeaders type exposes other properties that represent the WS-Addressing message headers. The idea here is that we can use the WCF object-oriented programming model to affect a service-oriented SOAP message.