This is probably the best place to get your hands dirty and learn about Soya and SSDL. We will walk you through a complete example application that creates a (quite silly actually) weather forecast service and a client service that communicates with it by sending messages back and forth.
We assume that you have read and performed the steps described in the Installation Instructions.
A lot of concepts in this tutorial will look familiar to developers that have used WCF before. If you are one of them, you probably know that is quite flexible and the same thing can be done in many different ways. In this example we show how a service in Soya/WCF can be created using the approach we like best without ruling out that other methods exist to achieve the same.
To be able to use Soya, you need to add a few class libraries to your project. Make sure that you include the .NET 3.0 libraries System.Runtime.Serialization.dll and System.ServiceModel.dll. The latter is the library that contains WCF. Furthermore, add the Soya.dll assembly from the directory where you have extracted the Soya distribution.
Also, if you don't want to end up staring at a blank console window, we recommend that you set up log4n as described in the Logging Section.
We will distinguish between several different steps to create the service, starting with defining the contract.
A service's contract is defined by messages that can be exchanged with the service and how they relate to each other. Hence, we first define some messages that our service will support.
public class NS { public const string Contract = "http://my-meteo.org/service/contract"; public const string Schema = "http://my-meteo.org/service/schema"; public const string Message = "http://my-meteo.org/service/message"; public const string Protocol = "http://my-meteo.org/service/protocol"; }
In order to avoid repeating XML namespaces throughout our message definitions, we declare them in a separate class:
The first message that we create is a message that other services can send to our service to request a weather forecast for a specific city. For some reason, we ask clients to include a username in the SOAP message headers along with the name of a city in the SOAP body. Further, we define using the Namespace parameter to which XML namespace the SSDL message as well as the header and body elements belong.
[SsdlMessageContract(Namespace = NS.Message)] public class ForecastRequestMsg { [MessageHeader(Namespace = NS.Schema, MustUnderstand = true)] // could be different ns public string UserName; [MessageBodyMember(Namespace = NS.Schema)] public string City; }
The second message that we create will be used to send forecast information in response to the request message we have defined above. This time the message defines no header but it uses a complex type in its body. Also, we override the default name of the element (which would be body) with a different name (i.e. Forecast).
[SsdlMessageContract(Namespace = NS.Message)] public class ForecastResponseMsg { [MessageBodyMember(Name = "Forecast", Namespace = NS.Schema)] public ForecastDetail body; }
The complex type is defined by another class that defines a DataContract attribute and DataMember fields for the fields that are to be serialized.
[DataContract(Namespace = NS.Schema)] public class ForecastDetail { [DataMember] public int Temperature; [DataMember] public string Condition; }
Now that we have defined the message contracts we need to indicate how they relate to each other, i.e. in which order they can be exchanged between services. We can capture this information in the service interface using the MEP protocol framework by decorating the service methods.
First, we define two attributes on the interface that control the XML namespaces of the generated SSDL contract. Then, we define a method called Process and add a Mep attribute with a Style = MepStyle.InOut parameter to indicate that we want to capture an In-Out Message Exchange Pattern (MEP). The name we choose for the method is irrelevant, because Soya dispatches messages according to protocol information, not operation names.
Please note that operations in SSDL are always one-way, i.e. they have no return or output values. If we want to model request-response semantics, for example, we define them using a protocol framework. Indeed, that is exactly what the MEP framework does. Other protocol frameworks, however, allow the definition of much more sophisticated messaging behaviors over and above simple request-response and its ilk. This has the positive side-effect of disallowing a one-to one mapping between message exchange patterns and API method invocations.
The incoming message is derived from the method signature (i.e. ForecastRequestMsg) whereas the outgoing message is specified with the Out parameter in the attribute declaration (i.e. ForecastResponseMsg).
[ServiceContract(Namespace = NS.Contract)] [SsdlProtocolContract(Namespace = NS.Protocol)] public interface IForecastService { [Mep(Style = MepStyle.InOut, Out = typeof(ForecastResponseMsg))] void Process(ForecastRequestMsg request); }
By creating the classes above and decorating them with various attributes, we have essentially created the SSDL contract (apart from the endpoint, which we will do further below). Here's the SSDL contract that Soya infers from what we've been doing so far:
<ssdl:contract xmlns:ssdl="urn:ssdl:v1" targetNamespace="http://my-meteo.org/service/contract"> <ssdl:schemas> <xs:schema xmlns:tns="http://my-meteo.org/service/schema" targetNamespace="http://my-meteo.org/service/schema" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="UserName" nillable="true" type="xs:string" /> <xs:element name="City" type="xs:string" /> <xs:complexType name="ForecastDetail"> <xs:sequence> <xs:element minOccurs="0" form="qualified" name="Condition" nillable="true" type="xs:string" /> <xs:element minOccurs="0" form="qualified" name="Temperature" type="xs:int" /> </xs:sequence> </xs:complexType> <xs:element name="ForecastDetail" nillable="true" type="tns:ForecastDetail" /> <xs:element name="Forecast" type="tns:ForecastDetail" /> </xs:schema> </ssdl:schemas> <ssdl:messages targetNamespace="http://my-meteo.org/service/message" xmlns:ns1="http://my-meteo.org/service/schema"> <ssdl:message name="ForecastRequestMsg"> <ssdl:header ref="ns1:UserName" mustUnderstand="true" relay="false" /> <ssdl:body ref="ns1:City" /> </ssdl:message> <ssdl:message name="ForecastResponseMsg"> <ssdl:body ref="ns1:Forecast" /> </ssdl:message> </ssdl:messages> <ssdl:protocols> <ssdl:protocol targetNamespace="http://my-meteo.org/service/protocol" xmlns:mep="urn:ssdl:mep:v1"> <mep:in-out xmlns:ns2="http://my-meteo.org/service/message"> <ssdl:msgref ref="ns2:ForecastRequestMsg" direction="in" /> <ssdl:msgref ref="ns2:ForecastResponseMsg" direction="out" /> </mep:in-out> </ssdl:protocol> </ssdl:protocols> </ssdl:contract>
Next, we implement the above service contract. To do so, we just create a new class that implements the IForecastService interface.
public class MyForecastService : IForecastService { public void Process(ForecastRequestMsg request) { // do something } }
Because we have previously defined that our input message is part of an in-out MEP, we need to make sure that our implemenation adheres to that contract by also sending the outgoing message.
For the sake of simplicity we will hardcode the Process() method to create a ForecastResponseMsg and send it back to the client. By calling SoyaServiceHost.Reply() the message will be sent using the same binding to where it came from. The endpoint address is determined by Soya by looking at the WS-Addressing ReplyTo header.
ForecastDetail detail = new ForecastDetail(); detail.Condition = "SUNNY"; detail.Temperature = 28; ForecastResponseMsg response = new ForecastResponseMsg(); response.body = detail; Message m = Commons.ConvertToMessage(response); SoyaServiceHost.Reply(m);
By declaring service endpoints we define how our service is exposed to the outside world, i.e. where it can be found, the transport protocol and message encoding it uses to send and receive messages, and so on. In our example we will do this declaratively by adding a new application configuration file (app.config) to our project.
Configuring the application endpoints in Soya is done exactly the same way as it is done in WCF. We define our service implementation, the address where we want our service to listen for new connections, the binding that we want to use and the service contract. In this example we are using the wsHttpBinding, which is an interoperable binding predefined by WCF that uses HTTP for communicating.
<configuration> <system.serviceModel> <services> <service name="Meteo.Service.MyForecastService"> <host> <baseAddresses> <add baseAddress="http://localhost:8080/meteo-service/"/> </baseAddresses> </host> <endpoint address="ws" binding="wsHttpBinding" contract="Meteo.Service.IForecastService" /> </service> </services> </system.serviceModel> </configuration>
In SSDL terms this will add the last element to our SSDL contract as shown below:
<-- upper part remains unchanged --> <ssdl:endpoints> <ssdl:endpoint xmlns:wsa="http://www.w3.org/2004/12/addressing"> <wsa:Address>http://localhost:8080/meteo-service/ws</wsa:Address> </ssdl:endpoint> </ssdl:endpoints> </contract>
Our work as service designers and developers is completed. All we need to do now is deploy our service. To do so, we instantiate a new SoyaServiceHost and pass it a reference to our service implementation and open the host.
SoyaServiceHost host = new SoyaServiceHost(typeof(MyForecastService)); host.Open();
If everything works as expected, you should see something similar to the following output on your console (given that you have setup logging). Alternatively, you can point your browser to http://localhost:8080/meteo-service?ssdl to retrieve the SSDL contract.
[INFO] Program:Main - Starting program... [INFO] SoyaServiceHost:PrintServiceDescription - MyForecastService running with following endpoints: Endpoints ********* Endpoint: address: http://localhost:8080/meteo-service/ws binding: WSHttpBinding contract: IForecastService
If you can't see any output or receive an error, please go back to the beginning of this document and make sure that you have followed all the steps correctly. A working example can be downloaded here or from the Soya Samples Section. Also, all samples are included in the Soya distribution.
Although we have deployed our service above and can look at the generated SSDL description, not much is happening, because no clients are yet interacting with it.
We could write any kind of client (e.g. in C#, Java, Ruby ...) that interacts with our service as long as it adheres to the SSDL contract defined by the service (i.e. compatible SOAP messages and correct message exchange sequence). Instead, we will create another SSDL service, because Soya can be used on both client and server side. For convenience we will refer to the two services as client and server, although this naming is not really correct because both services can be either one or the other. It only depends on the role they take on, but it is easily conceivable that even in the same conversation one service acts as a client and later on as a server. As a convention, we will hence use client for the service that starts the conversation.
We proceed exactly the same for creating the client service as we did for creating the server service: We create a new project, add the required references and define message, data and service contracts. Although we will just copy the message and data contract classes (we won't repeat them again here) we note that those classes don't have to be the same as on the server, however they need to be compatible in terms of the SOAP messages that they will produce.
The service interface is more interesting. We define an interface (note that we don't have to reuse the same name as on the server) and declare a method called ProcessResponse. Again, the name of the method is irrelevant. This time, however, we use a out-in MEP pattern to capture the expected message interactions. In this pattern the incoming message is derived from the method signature whereas the outgoing message needs to be specified as an additional Out parameter in the attribute declaration.
[ServiceContract(Namespace = NS.Contract)] [SsdlProtocolContract(Namespace = NS.Protocol)] public interface IClientService { [Mep(Style = MepStyle.OutIn, Out = typeof(ForecastRequestMsg))] void ProcessResponse(ForecastResponseMsg response); }
Again, we have created an SSDL contract (apart from the endpoint, which we will do further below). Because the schemas and messages are the as they are for the server, we will just show the protocols section that Soya has inferred:
<ssdl:protocols> <ssdl:protocol targetNamespace="http://my-meteo.org/service/protocol" xmlns:mep="urn:ssdl:mep:v1"> <mep:out-in xmlns:ns2="http://my-meteo.org/service/message"> <ssdl:msgref ref="ns2:ForecastResponseMsg" direction="in" /> <ssdl:msgref ref="ns2:ForecastRequestMsg" direction="out" /> </mep:out-in> </ssdl:protocol> </ssdl:protocols>
Analogously to the server side, we implement the service interface. However, this time, because ProcessResponse() will be called when a response to our initiating message (which we will implement shortly) arrives, there is no need to create a reply message. Instead, we just output the content of the incoming message.
public class MyClientService : IClientService { public void ProcessResponse(ForecastResponseMsg response) { ForecastDetail forecast = response.body; Console.WriteLine("Received forecast from server: {0} - {1}", forecast.Condition, forecast.Temperature); } }
Similarly to how we defined endpoints for the server, we add an application configuration file (app.config) to our project. Because our client in reality is also a server that listens for messages, we need to define a server section.
However, because the client also initiates a conversation, we need to give it some additional information so it knows where the first message in the conversation needs to be sent. Therefore, we also define a client section that points to the remote endpoint. Because Soya handles the messages that our service implementation sends based on the SSDL contract information, we don't really care what kind of output contract we're using. Hence, we just declare System.ServiceModel.Channels.IOutputChannel as the remote endpoint contract, which is a generic contract for sending (one-way) messages.
<configuration> <system.serviceModel> <services> <service name="Meteo.Client.MyClientService"> <host> <baseAddresses> <add baseAddress="http://localhost:8081/client/"/> </baseAddresses> </host> <endpoint address="ws" binding="wsHttpBinding" contract="Meteo.Client.IClientService" /> </service> </services> <client> <endpoint address="http://localhost:8080/meteo-service/" binding="wsHttpBinding" contract="System.ServiceModel.Channels.IOutputChannel"/> </client> </system.serviceModel> </configuration>
After we have created our client service it's time to deploy it. First, we need to instantiate a new SoyaServiceHost as we have done previously on the server that will be responsible for handling incoming messages. Additionally, we create the first message and initiate the conversation by sending it to the remote service.
// open service that will process incoming messages SoyaServiceHost host = new SoyaServiceHost(typeof (MyClientService)); host.Open(); // create outgoing message ForecastRequestMsg request = new ForecastRequestMsg(); request.UserName = "patforna"; request.City = "Sydney,Australia"; Message m = Commons.ConvertToMessage(request); // obtain client from running host and send message SoyaClient client = host.GetClient(); client.Send(m);
If everything works as expected, you should see something similar to the following output on your client console (given that you have setup logging). Also, you can also point your browser to http://localhost:8081/client?ssdl to retrieve the SSDL contract.
[INFO ] Program:Main - Starting program... [INFO ] SoyaServiceHost:PrintServiceDescription - MyClientService running with following endpoints: Endpoints ********* Endpoint: address: http://localhost:8081/client/ws binding: WSHttpBinding contract: IClientService [INFO ] MyClientService:ProcessResponse - Received forecast from server: SUNNY - 28
For the sake of completeness, we have listed the SOAP messages that are being exchanged between the two services below.
<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:soya:action</a:Action> <h:UserName s:mustUnderstand="1" xmlns:h="http://my-meteo.org/service/schema">patforna</h:UserName> <a:ReplyTo> <a:Address>http://localhost:8081/client/ws</a:Address> </a:ReplyTo> <a:MessageID>urn:uuid:5579d116-2f9e-4dba-a305-434aeb3489d4</a:MessageID> <a:To s:mustUnderstand="1">http://localhost:8080/meteo-service/ws</a:To> </s:Header> <s:Body> <City xmlns="http://my-meteo.org/service/schema">Sydney,Australia</City> </s:Body> </s:Envelope>
<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:soya:action</a:Action> <a:ReplyTo> <a:Address>http://localhost:8080/meteo-service/ws</a:Address> </a:ReplyTo> <a:MessageID>urn:uuid:10bd5f39-8f47-493d-b33b-b04247fafcbe</a:MessageID> <a:RelatesTo>urn:uuid:5579d116-2f9e-4dba-a305-434aeb3489d4</a:RelatesTo> <a:To s:mustUnderstand="1">http://localhost:8081/client/ws</a:To> </s:Header> <s:Body> <Forecast xmlns="http://my-meteo.org/service/schema" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"> <Condition>SUNNY</Condition> <Temperature>28</Temperature> </Forecast> </s:Body> </s:Envelope>