Contents
Note: This article was written using .NET Framework 3.0 - June 2006 CTP.
The Web Service Transfer (WS-Transfer) represents a specification to capture the current or intended state of the resource and transferring that representation between components. It is an abstraction of the architectural elements within distributed heterogeneous systems. The WS-Transfer encapsulates an implementation and maintenance of the resource from its representation. WS-Transfer uses a uniform interface between the components, based on the SOAP protocol, allowing to trade-off the state of resources on the network. I also recommend you to read the [2] - Representational State Transfer (REST) document that gives a detailed description of the resource oriented architectural style.
Let me briefly describe the major architectural elements - the keys of WS-Transfer, for better understanding and position in the application model. We can create a logical model "of the exchange state of resources on the Service Bus", based on these elements.
Resource
The resource represents the key abstraction in the WS-Transfer specification. The resource can be any item of interest/information that can be named. For instance, this article, web page, image, service information, handler to the file, collection of resources and/or resource identifiers; it can also be a SOAP message, SOAP header, etc. Therefore, the WS-Transfer is a resource oriented architectural style where a resource can be a static information, for instance - an image of the Title document, or dynamic information such as weather, stock market, resource handlers, etc.
Resource identifier
The resource identifier represents a logical entity of the resource, the value associated with the specific "simple" resource. The resource identifier enables to identify the resource or its parts. For example, this article is an example of an information resource. It consists of words, pictures, code snippets, etc. The Code Project factory will store its state under the well-known identifier. Another great example is the Google search engine: you as a web site visitor, type the search key to the Google factory. In this case, the key represents your initiated resource state. Based on this state, the factory will return a representation of the resource identifier, which is a collection of resource parts (resource identifiers).
Representation
A representation is a concrete form of the resource state metadata, describing (encoding) the data. It is a manifestation of the resource. The representation can be included in a message that travels between the endpoints. For example: a SOAP message payload is a representation of the resource state, using the well-known XML formatted text.
Interface
The WS-Transfer defines a mechanism for acquiring XML-based representations of a resource; how it may be created, sent, or received using the interaction protocols. This interaction is grouped into the following functional groups:
- Resource factory: responsible to create a resource state from an XML representation and return its identifier.
- Resource operation: responsible to manipulate a resource state such as: to get, put, and delete a resource state.
Notice that WS-Transfer does not define a lifetime and maintenance of the resource state. Practically, if the client has an address of the endpoint where the resource state and its identifier exist, it does not mean that the operation will be a success. For example: the resource state will be temporarily unavailable, for instance, when the file system is recovering, etc.
There are four operations of the WS-Transfer:
- Create: specifies the creation of the resource, and initiates its representation (it can also be null). This is a factory interface (only one in this group) with a bidirectional messaging. The first request will create a resource and initialize its state, the second one - its response will return back an address with a resource identifier of the resource state representation. Notice that the resource state can also be an information about the next resource factory, handlers, resource descriptors, image body, and so on.
- Get: specifies a message protocol for fetching a resource state in the XML representation based on the resource identifier included in the address header.
- Put: specifies a message protocol for updating (replacing) the resource state by a new resource representation. The resource may reject this update if it does not match with its initialized representation.
- Delete: specifies a message protocol to delete a resource state.
As I mentioned above, the resource factory will return an address of the service where the resource state exists. This resource address is used implicitly for the resource operation, so the client doesn't need to know about its representation and it will use it as it is. It looks like the resource address is a logical token of the resource operation.
Let me describe the WS-Transfer interaction between the resource and its component (client) using the following picture:
The client (web application, web service, etc.) needs to know the endpoint of the resource factory, for instance, the EP 1. Sending the create message to this endpoint with the initiated resource representation will force the factory to create a resource state. For example, the weather information in the LAX. In order to obtain this state (information/resource), the client needs to know its address and identifier, which it will receive via the create response in the message body. This is the first step.
The second step is to process a resource operation. The client will take this resource address by calling the resource operation. For instance, the get operation. The operation response will return an XML-based representation of the resource state, for example, weather conditions in LAX. Based on the resource factory (service), the client can send another operation such as put and delete. In our example, the resource operation service will not have an implementation of the put and/or delete interfaces. It does make sense, that the client cannot update the weather conditions in LAX, doesn't it? By the way, it would be nice to have a service like this.
Therefore, based on the above picture, we know that the first call needs to be addressed to the factory, and then the next call to the operation services. It is up to the resource factory and operation services to decide how the resource state will be created, along with its lifetime and operation. In addition, the resource factory and the operation can be handled by the same service.
This two step fashion design pattern is logically correct; therefore, the client knows "where" the resource factory exists for this resource type. The response of the first step call will contain the explicit details (handlers/identifier/etc.) about the resource address. For example - writing bytes to the file system:
FileStream factory = new System.IO.FileStream(myResource,
FileMode.Create);
factory.Write(myResourceStateRepresentation, 0, 100);
factory.Close();
The first line in the above analogy calls the file factory to create the resource - a file stream. The return value is the address of the resource which can accept an operation such as write (put). The resource will be updated with a new representation during this operation. After that, the factory can be closed with a new resource state.
A WS-Transfer interaction is based on the REST [2] architectural style (it looks like its SOAP implementation) where four interfaces can handle the creation and operation of the resource state. There is a CRUD (Create/Retrieve/Update/Delete) style in the resource state, the same way it is in the database layer - database record.
By the way, this kind of an architecture style also exists in the CPU hardware world, see CISC (Complex Instruction Set Computer) vs. RISC (Reduced Instruction Set Computer).
Anyway, based on the above description, let me step into a more practical example where WS-Transfer plays a significant role in the multi-tier and multi-layer architecture such as encapsulation of the logical business layer from the physical sources driven by different protocols, binding, etc.
Example 1 - Viewing images
Let's take the example of viewing document images from different repositories. The following picture shows layering of a model where the Front-End Business Layer (FEBL) is logically connected to the "Service Bus" that understands the WS-Transfer access to the state representation of the resources. The services on the bus such as ImagingService and SourceService provide the creation of the resource descriptor - handler to map the logical business info into the physical parameters requested by each source adapter and resource operation for the Get resource state representation. The StorageService can be used for storing a resource descriptor of the image and/or the image body.
The service bus driven by WS-Transfer enables tearing of the layers that can be hosted in the common process (for instance, IIS), or deployed on the enterprise network. The presentation layer, with its business pre-processors located in the FEBL, has access to the physical images in a transparent manner, based on the logical business info, and metadata such as document identifier, type, etc. Note that the service bus represents a logical layer of the services, with a uniform interface based on the WS-* (wsProfileBinding
). The other non-WS spec interface can be handled via the adapters.
The following picture shows an interaction protocol to the WS-* driven service bus, specifically with WS-Transfer. The application layer (FEBL) sends a wrapped resource with a Logical Resource Descriptor (LRD) to a well known resource factory located at EP1. The LRD represents a DataContract representation of this resource type between the resource factory and its consumer. From the WS-Transfer specification point of view, it can be considered as a resource with its resource state. Based on the LRD, the resource factory will create a resource state. In our case, the response will represent a Physical Resource Descriptor (PRD), which is the resource identifier:
Once an application has a response from the resource factory, it can process a resource operation, such as the Get (image properties and/or image body) in our example. As the above example shows, the application layer is encapsulated from the source in a transparent loosely coupled manner, using a uniform layer interaction. Of course, this interaction stack can be expanded for security, policy, etc., the stack elements based on the deployment schema.
In addition, this example shows that the resource state can be considered as a resource identifier for the target resource, using a simple logic of physical mapping in the resource factory. This design pattern fashion can be applied mostly to any resource type, interactions, etc. Based on the resource type, we can build a specific resource factory such as ImageService, StorageService, and so on. Of course, some services can support an unknown ("naked") resource type like the one defined by the WS-Transfer/Create specification.
There is nothing wrong if the resource has another wrapper that includes more metadata in its resource descriptor. The main rule in the layered distributed transport paradigm is "don't open the package". For instance, the FedEx; you come to them (factory) with your package (resource), you fill-up their form (resource descriptor), and then all of it is packaged (factory) into the FedEx envelope. You will receive a FedEx factory response, represented by a package ID (resource identifier), which can be used for the resource (package) operation. Can you see an analogy to the above ImageService?
Let's look at the create request/response messages. The following code snippet shows the payload of the create message. This payload is a representation of the resource state, with the local name CreateResource
. I created it in my namespace to demonstrate the wrapping of a "naked" resource, with its descriptor represented by ResourceDescriptor
.
The ResourceDescriptor
is a complex type of the resource identifier. There are many useful properties such as the key, topic, ID, expires, notification (subscriptions), source metadata, etc., describing the resource state and its lifetime. For instance, when a resource state has been updated, the notification can be sent to the subscriber based on his/her subscriptions. Another example: since the lifetime of the resource state can be limited, the subscriber can be notified before the resource state expires.
Back to the following code snippet. The bolded section shows an actual WS-Transfer resource state in the create request, and how it is received by the resource factory:
="1.0" ="utf-16"
<soap:Envelope>
<soap:Header>
<wsa:Action >
http://schemas.xmlsoap.org/ws/2004/09/transfer/Create
</wsa:Action>
<wsa:To>net.tcp://myServer:12345/PublicImageService</wsa:To>
</soap:Header>
<soap:Body>
<CreateResource
xmlns="http://www.rkiss.com/schemas/sb/2005/06/servicebus/xfer">
<ResourceDescriptor>
<Name>Example of the Image Resource</Name>
<Topic>Image</Topic>
<Key>image:123456789</Key>
<Expires>2006-06-26T21:07:00</Expires>
<Notify />
</ResourceDescriptor>
<Resource />
</CreateResource>
</soap:Body>
The following code snippet shows a create response from the resource factory. I have bolded the section that shows a resource identifier. Note that WS-Transfer does not specify the type of the resource identifier. It can be of type URI or complex type, for instance, my ResourceDescriptor
. The reason is that this resource identifier is an information (metadata) for the resource operation, and not for the client. The client should not care about the content of the resource identifier. It's like the file handler, FedEx's tracking number in the above example:
="1.0" ="utf-8"
<soap:Envelope>
<soap:Body>
<wxf:ResourceCreated xmlns:wxf=
"http://schemas.xmlsoap.org/ws/2004/09/transfer">
<wsa:Address>net.tcp://myServer2:12345/PublicSourceService
</wsa:Address>
<wsa:ReferenceProperties>
<ResourceDescriptor xmlns=
"http://www.rkiss.com/schemas/sb/2005/06/servicebus/xfer">
<Topic>Image</Topic>
<Identifier>uuid:11111111-1111-1111-1111-111111111111
</Identifier>
<Expires>2006-06-27T11:07:00Z</Expires>
<Created>2005-07-05T04:16:37Z</Created>
<Source>
<wsa:Address>http://ImageServer/ws/imageWebService.asmx
</wsa:Address>
<wsa:ReferenceProperties>
<Proxy type="ImageAdapters.Adapter1, MyAdapter">
<Method name="GetImageBody" />
<Parameter name="Username" value="myName" />
<Parameter name="Password" value="myPassword" />
<Parameter name="Location" value="California" />
<Parameter name="DocumentIdentifier"
value="ABCD:123456789" />
</Proxy>
</wsa:ReferenceProperties>
</Source>
</ResourceDescriptor>
</wsa:ReferenceProperties>
</wxf:ResourceCreated>
</soap:Body>
</soap:Envelope>
Notice that wsf:ResourceCreated
is the sub element (in the class terminology - subclass of the EndpointAddress
). By the way, this class is a sealed
class. It would be nice to have it without this restriction in the release version, like an EndpointReference
in the WSE2 of wsa:Address
, where wsa:RefererenceProperties
carries our resource identifier for the resource operations. It simplifies the usage of the WS-Transfer interaction using a "I am forwarding what I received" fashion.
I verified the above WS-Transfer factory and operation, including the ResourceDescriptor
representation using the WSE 2 technology. Let's see how WS-Transfer can be implemented using the Indigo technology.
The following chapters focus on the concept, design, and implementation of the WS-Transfer spec by the Indigo communication model. I assume that the readers understand this model and its famous ABC (Address, Binding, Contract) pattern.
I am going to describe:
- Creating the request and response messages - its consumer and provider methods.
- Custom binding - how to add your configuration properties in the binding process.
- Encapsulation of the custom message processing in the service adapter.
In addition, you will also get a simple console logger as an example of the MessageInspector implementation. This simple logger is used to demonstrate and test the WS-Transfer interaction between the console-console host programs. Of course, you can also use Indigo built-in diagnostics features.
The service driven by WS-Transfer messaging has the following features:
- Create resource state.
- Get resource state.
- Put (Update) resource state.
- Delete resource state.
- Encapsulation of the WS-Transfer interface from the service adapter.
- DataContract for resource identifier.
- Configuration using an endpoint binding.
- Plug-in a configurable service adapter.
- One service can handle multiple adapters.
The concept of the implementation is based on encapsulating an application layer driven by CLR objects from the resource (physical) layer, using the Indigo paradigm. The following picture shows these three layers:
The connector represents the client side - the business layer, where the consumer is connected using the RPC fashion. The connector's responsibility is to transform the call into a SOAP XML formatted message based on the WS-Transfer specification, and pass it via an Indigo infrastructure to the resource specific adapter. In this scenario, the Indigo represents the logical model of connectivity between the resource and its consumer. It is a mechanism of how to tear a business layer from the physical layer. I am using the same end's naming, like BizTalk (Connector - Adapter).
The components of the WS-Transfer implementation are divided into the following parts:
Level 0 (core)
- ServiceContract
- Request/Response messages
Level 1 (service layer)
- Configuration
- Service
- Adapter
Level 2 (consumer)
Level 0 - core
This is the core of the WS-Transfer implementation. It is possible to implement the highest level of WS-Transfer features based on the ServiceContract
and request/response messages. This ability will simplify its usage.
The ServiceContract
is a declaration of the WS-Transfer interface. The following code snippet shows its abstract declaration. As you can see, there are two groups of interfaces, such as factory interface and operation interface, that allow support for factory and operation services, respectively. In the case of the common WS-Transfer service, the IWSTransfer
can be used:
[ServiceContract(Namespace = WSTransfer.NamespaceUri)]
public interface IWSTransfer : IWSTransferFactory,
IWSTransferOperation
{
}
[ServiceContract(Namespace = WSTransfer.NamespaceUri)]
[XmlSerializerFormat]
public interface IWSTransferOperation
{
[OperationContract(Action = WSTransfer.GetAction,
ReplyAction = WSTransfer.GetResponseAction)]
GetResponse Get(GetRequest request);
[OperationContract(Action = WSTransfer.PutAction,
ReplyAction=WSTransfer.PutResponseAction)]
PutResponse Put(PutRequest request);
[OperationContract(Action = WSTransfer.DeleteAction,
ReplyAction = WSTransfer.DeleteResponseAction)]
DeleteResponse Delete(DeleteRequest request);
}
[ServiceContract(Namespace = WSTransfer.NamespaceUri)]
[XmlSerializerFormat]
public interface IWSTransferFactory
{
[OperationContract(Action = WSTransfer.CreateAction,
ReplyAction = WSTransfer.CreateResponseAction)]
CreateResponse Create(CreateRequest request);
}
The above interfaces use the Request
/Response
objects - message pattern allowing to plug-into the Indigo messaging paradigm in a transparent manner, without any knowledge of the "SOAP/XML world". These objects are transformers of the RPC calls to/from the WS-Transfer representation. In a way, the WSE2 messaging uses the IXmlElement
plumbing interface for transferring objects to/from the XML formatted document; Indigo is using a MessageContractAttribute
for controlling the insertion of custom headers and the body of the message during run-time.
Let's look at a couple of interface operations of the WS-Transfer actions implementation in the following code snippets "in which they show this design pattern". Their implementations are stereotyped in a fashion of transferring object properties to/from an XML formatted document, using a MessageContract
within the OperationContract
.
Note that the service contract is decorated to use the XmlSerializer
(instead of the XmlFormatter
) for serialization/deserialization of a message stream. This pattern allows handling XML elements programmatically, based on the WS-Transfer spec requirements.
CreateRequest
WS-Transfer specification:
<s:Envelope ...>
<s:Header ...>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Create
</wsa:Action>
<wsa:MessageID>xs:anyURI</wsa:MessageID>
<wsa:To>xs:anyURI</wsa:To>
...
</s:Header>
<s:Body ...>
xs:any
</s:Body>
</s:Envelope>
Create request implementation:
[MessageContract]
public class CreateRequest
{
#region DataMembers
private XmlElement _resource;
#endregion
#region Properties
[MessageBodyMember(Order = 0)]
[XmlAnyElement]
public object Resource
{
get { return _resource; }
set { _resource = Convert.ObjectToXml(value); }
}
#endregion
#region Constructors
public CreateRequest() : this((object)null)
{
}
public CreateRequest(object resource)
{
this.Resource = resource;
}
#endregion
}
and an example:
<soap:Envelope>
<soap:Header>
<wsa:Action >
http://schemas.xmlsoap.org/ws/2004/09/transfer/Create
</wsa:Action>
<wsa:To>net.tcp://myServer:12345/PublicStorageService
</wsa:To>
<wsa:MessageID>urn:uuid:00000000-0000-0000-C000-000000000048
</wsa:MessageID>
</soap:Header>
<soap:Body>
<xxx:Customer>
<xxx:First>Roman</xxx:First>
<xxx:Last>Kiss</xxx:Last>
</xxx:Customer>
</soap:Body>
</soap:Envelope>
or another example, where the message body is a DataContract
object, for instance, CreateResource
:
<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">
http://schemas.xmlsoap.org/ws/2004/09/transfer/Create
</wsa:Action>
<wsa:MessageID>urn:uuid:d4020733-ced1-44ae-9728-e26c8ce457ca;id=0
</wsa:MessageID>
</s:Header>
<s:Body>
<CreateResource
xmlns="http://www.rkiss.com/schemas/sb/2005/06/servicebus/xfer">
<ResourceDescriptor>
<Identifier>12345678</Identifier>
</ResourceDescriptor>
<Resource />
</CreateResource>
</s:Body>
</s:Envelope>
CreateResponse
WS-Transfer specification:
<s:Envelope ...>
<s:Header ...>
<wsa:Action>
http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse
</wsa:Action>
<wsa:RelatesTo>xs:anyURI</wsa:RelatesTo>
<wsa:To>xs:anyURI</wsa:To>
...
</s:Header>
<s:Body ...>
<wxf:ResourceCreated>endpoint-reference
</wxf:ResourceCreated>
xs:any
</s:Body>
</s:Envelope>
Create response implementation:
[MessageContract]
public class CreateResponse
{
#region DataMembers
private EndpointAddress _epa;
#endregion
#region Properties
[MessageBodyMember(
Name=WSTransfer.ElementNames.ResourceCreated,
Namespace=WSTransfer.NamespaceUri, Order=0)]
public EndpointAddress10 EndpointAddress10
{
get { return EndpointAddress10.FromEndpointAddress(_epa); }
set { _epa = value.ToEndpointAddress(); }
}
public EndpointAddress EndpointAddress
{
get { return _epa; }
set { _epa = value; }
}
#endregion
#region Constructors
public CreateResponse()
{
this.EndpointAddress = null;
}
public CreateResponse(EndpointAddress epa)
{
this.EndpointAddress = epa;
}
#endregion
}
and an example:
<soap:Envelope>
<soap:Header>
<wsa:Action >
http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse
</wsa:Action>
<wsa:To s:mustUnderstand="1">
http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
</wsa:To>
</soap:Header>
<soap:Body>
<wxf:ResourceCreated>
<wsa:Address>http://www.example.org/someport</wsa:Address>
<wsa:ReferenceProperties>
<xxx:CustomerID>123456789</xxx:CustomerID>
</wsa:ReferenceProperties>
</wxf:ResourceCreated>
</s:Body>
</s:Envelope>
Another example that shows ReferenceProperties
has a DataContract
(resource identifier) object for instance: ResourceDescriptor
. Notice again, that the resource identifier must be recognized by the WS-Transfer Operation service addressed by its factory service. This is the WS-Transfer architectural magic style ("black box token"):
<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">
http://schemas.xmlsoap.org/ws/2004/09/transfer/CreateResponse
</wsa:Action>
<wsa:RelatesTo>urn:uuid:d4020733-ced1-44ae-9728-e26c8ce457ca;id=0
</wsa:RelatesTo>
<wsa:To s:mustUnderstand="1">
http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
</wsa:To>
</s:Header>
<s:Body>
<wxf:ResourceCreated
xmlns:wxf="http://schemas.xmlsoap.org/ws/2004/09/transfer">
<wsa:Address>http://myServer2/WebHost/PublicSourceService.ashx
</wsa:Address>
<wsa:ReferenceProperties>
<ResourceDescriptor
xmlns="http://www.rkiss.com/schemas/sb/2005/06/servicebus/xfer">
<Topic>Image</Topic>
<Expires>2006-06-27T11:07:00Z</Expires>
<Created>2005-07-05T04:16:37Z</Created>
<Source>
<wsa:Address>http://ImageServer/ws/imageWebService.asmx
</wsa:Address>
<wsa:ReferenceProperties>
<Proxy type="ImageAdapters.Adapter1, MyAdapter">
<Method name="GetImageBody" />
<Parameter name="DocumentIdentifier"
value="ABCD:123456789" />
</Proxy>
</wsa:ReferenceProperties>
</Source>
</ResourceDescriptor>
</wsa:ReferenceProperties>
</wxf:ResourceCreated>
</s:Body>
</s:Envelope>
That is all for the request/response messages for the WS-Transfer factory interface. What we have is the following: the request message sends any payload of the resource to the factory, and the factory responds with a CreateResponse
body, which is a "ticket" for the resource operation. Let's describe how the GetRequest/Response messages can be transferred in Indigo:
GetRequest
We have a little bit different scenario for the operation contract. The EndpointAddress
header needs to pass a resource identifier in the ReferenceProperties
. Note that this address is returned by the resource factory. The following code snippet shows how the GetRequest
message can be created using the MessageHeaderAttribute
.
WS-Transfer specification:
<s:Envelope ...>
<s:Header ...>
<wsa:Action>http://schemas.xmlsoap.org/ws/2004/09/transfer/Get
</wsa:Action>
<wsa:MessageID>xs:anyURI</wsa:MessageID>
<wsa:To>xs:anyURI</wsa:To>
...
</s:Header>
<s:Body .../>
</s:Envelope>
Get request implementation:
[MessageContract]
public class GetRequest
{
#region DataMembers
private XmlElement _identifier = null;
#endregion
#region Properties
[MessageHeader]
[XmlAnyElement]
public object Identifier
{
get { return _identifier; }
set { _identifier = Convert.ObjectToXmlAnyElement(value); }
}
#endregion
#region Constructors
public GetRequest()
{
}
public GetRequest(object identifier)
{
this.Identifier = identifier;
}
#endregion
}
An example with the ResourceDescriptor
:
<soap:Envelope>
<soap:Header>
<wsa:Action >
http://schemas.xmlsoap.org/ws/2004/09/transfer/Get
</wsa:Action>
<wsa:To>net.tcp://myServer:12345/PublicStorageService
</wsa:To>
<wsa:MessageID>urn:uuid:94705fae-90e1-462a-969f-23d609c06073;id=0
</wsa:MessageID>
<ResourceDescriptor x:Id="1"
xmlns="http://www.rkiss.net/schemas/sb/2005/06/servicebus"
xmlns:x="http://schemas.microsoft.com/2003/10/Serialization/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<Key xsi:nil="true"></Key>
<Name xsi:nil="true"></Name>
<ResourceId x:Id="2">guid:142dae63-d886-4201-8c25-97f2f6be41b4
</ResourceId>
<Topic xsi:nil="true"></Topic>
</ResourceDescriptor>
</s:Header>
<soap:Body/>
</soap:Envelope>
GetResponse
WS-Transfer specification:
<s:Envelope ...>
<s:Header ...>
<wsa:Action>
http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse
</wsa:Action>
<wsa:RelatesTo>xs:anyURI</wsa:RelatesTo>
<wsa:To>xs:anyURI</wsa:To>
...
</s:Header>
<s:Body ...>
xs:any
</s:Body>
</s:Envelope>
Based on the WS-Transfer spec, the GetResponse
must return a representation of the resource state in the message body. The message is produced based on the resource object type. For the XmlElement
type, the resource body is directly written to the message body using the helper class, see the following code snippet. I like this straightforward pattern:
[MessageContract]
public class GetResponse
{
#region DataMembers
private XmlElement _resource;
#endregion
#region Properties
[MessageBodyMember]
[XmlAnyElement]
public object Resource
{
get { return _resource; }
set { _resource = Convert.ObjectToXml(value); }
}
#endregion
#region Constructors
public GetResponse()
{
this.Resource = null;
}
public GetResponse(object resource)
{
this.Resource = resource;
}
#endregion
}
and an example:
<soap:Envelope>
<soap:Header>
<wsa:Action>
http://schemas.xmlsoap.org/ws/2004/09/transfer/GetResponse
</wsa:Action>
<wsa:To s:mustUnderstand="1">
http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous
</wsa:To>
<wsa:RelatesTo>uuid:94705fae-90e1-462a-969f-23d609c06073
</wsa:RelatesTo>
</soap:Header>
<soap:Body>... stream ...</soap:Body>
</soap:Envelope>
As the above code snippets show, the message header or the body can be any object, therefore we decorated them with an XmlAnyElement
attribute. For this formatting fashion, any produced value must be converted to the XmlElement
. The following code snippet shows this helper static
method:
public static XmlElement ObjectToXmlAnyElement(object resource)
{
if (resource != null)
{
if (resource is XmlElement)
{
return resource as XmlElement;
}
else if (resource is XmlNode)
{
return resource as XmlElement;
}
else
{
XmlDocument doc = new XmlDocument();
using (MemoryStream ms = new MemoryStream())
{
XmlWriter xtw = XmlTextWriter.Create(ms, null);
xtw.WriteStartElement("root");
XmlSerializer xs = new XmlSerializer(resource.GetType());
xs.Serialize(xtw, resource);
xtw.WriteEndElement();
xtw.Flush();
ms.Position = 0;
doc.Load(ms);
}
return doc.DocumentElement;
}
}
return null;
}
The rest of the implementation of the operation messages such as Put
and Delete
request/response classes are very similar to the Get
messages. I will skip that part in this article, so that we can now focus on the Level 1 that represents a usage of the described messages.
Level 1 - service layer
This is a factory and operation service layer to encapsulate the WS-Transfer messaging from the resource logic. The service layer can be implemented in many design patterns. Basically, this level should have a generic service layer driven by WS-Transfer messaging and a loosely coupled adapter, where a resource specific logic can be implemented. The following picture shows this pattern:
As the above picture shows, the WSTransferService
layer has virtual access to the resource factory and operation. The resource logic has been decoupled into the resource type oriented adapters such as MemoryStorageAdapter
, ImageAdapter
, SqlStorageAdapter
, and so on. The adapter and WSTransferService
are plumbed together via the IWSTransferAdapter
interface, in a loosely coupled manner, based on the configuration behavior extension. This pattern allows to have multiple adapters under one WS-Transfer service contract (with different endpoints, of course).
For instance: the following class shows plumbing a MemoryStorageAdapter
into the WSTransferService
in a tightly coupled manner:
public class WSTransferService :
WSTransferService<MemoryStorageAdapter> { }
and here is the loosely coupled plumbing of the "Configured Adapter", based on the CONFIG file:
public class WSTransferService :
WSTransferService<ConfiguredAdapter> { }
where WSTransferService<ServiceAdapter
>
class represents a generic WS-Transfer service and all plumbing abstraction:
public class WSTransferService<ServiceAdapter>
: ServiceAdapterBase<ServiceAdapter, IWSTransferAdapter>,
IWSTransfer where ServiceAdapter: class { }
The above WSTransfer service will need the following components:
IWSTransferAdapter
- interface to the adapter.
ServiceAdapterBase<ServiceAdapter, IWSTransferAdapter>
- base class for plumbing a virtual adapter to the service.
WSTransferService<ServiceAdapter>
- this is a service layer for WS-Transfer messaging.
and of course, the same generic (reusable) mechanism for extending the service/endpoint behavior:
ServiceAdapterBehaviorSection
- to create a service/endpoint custom behavior for a specific adapter via a CONFIG file.
AdapterAttribute
- to create a service/endpoint custom behavior for a specific adapter via the class/method attribute.
AdapterInvoker
- to initiate an instance of the adapter on the operation (method) level.
Configuration - Custom behavior extension
I will describe the above components from the bottom - the configuration extension. The following code snippet shows a part of the server CONFIG file. It is a straightforward standard configuration with a behavior extension - see the bolded text. The behaviorExtensions
allows to bind the adapter properties with a service (or endpoint). Note that the type
property is a mandatory attribute, the other attributes are passed like unrecognized attributes to the adapter instance.
Server host CONFIG:
<system.serviceModel>
<services>
<service name="RKiss.WSTransfer.WSTransferService"
behaviorConfiguration="WxfServiceExtension" >
<endpoint
address="net.tcp://localhost:11111/PublicStorage"
binding="customBinding"
bindingConfiguration="Binding1"
contract="RKiss.WSTransfer.IWSTransfer"/>
</service>
</services>
<bindings>
<customBinding>
<binding name="Binding1">
<tcpTransport/>
</binding>
</customBinding>
</bindings>
<behaviors>
<serviceBehaviors>
<behavior name="WxfServiceExtension" >
<logger enable="true" logAfterReceiveRequest="true"
logBeforeSendReply="true"/>
<wsTransferAdapter name="PublicStorage" topic="weather"
type="RKiss.WSTransfer.Adapters.MemoryStorageAdapter"/>
</behavior>
</serviceBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="logger" type="RKiss.Logger.LoggerBehaviorSection,Logger,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
<add name="wsTransferAdapter"
type="RKiss.WSTransfer.ServiceAdapterBehaviorElement,WSTransfer,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
</system.serviceModel>
The service/endpoint extensions design pattern is straightforward, well designed, and require minimum coding. Basically, we need three classes for handling this extension, such as a class for the configuration section, a class for attributing, and a class for the operation extension - interceptor. The extension state for holding configurable properties is the AdapterAttribute
class. The following code snippets show a major part of their implementation. Thanks to WCF (also known as Indigo) programming for minimizing this coding:
The class for the CONFIG section is shown in the following code snippet:
public class ServiceAdapterBehaviorSection : BehaviorExtensionSection
{
protected override object CreateBehavior()
{
string type = (string)this["type"];
Type adapterType = (type != null && type.Trim().Length > 0)
? Type.GetType(type) : null;
AdapterAttribute adapter = new AdapterAttribute(adapterType,
_unrecognizedAttributes);
return adapter;
}
}
The following class is an AdapterAttribute
derived from interfaces where a custom extension is done. Note that the operation extension cannot be done by the configuration section:
public class AdapterAttribute : Attribute,
IOperationBehavior, IEndpointBehavior, IServiceBehavior
{
public void ApplyDispatchBehavior(OperationDescription description,
DispatchOperation dispatch)
{
if (dispatch.Invoker is AdapterInvoker)
return;
dispatch.Invoker = new AdapterInvoker(Name, AdapterType,
Parameters, dispatch.Invoker);
}
public void ApplyDispatchBehavior(ServiceDescription description,
ServiceHostBase serviceHostBase)
{
foreach (ChannelDispatcher cd in serviceHostBase.ChannelDispatchers)
{
foreach (EndpointDispatcher epdispatch in cd.Endpoints)
{
ApplyDispatchBehavior(null, epdispatch);
}
}
}
public void ApplyDispatchBehavior(ServiceEndpoint serviceEndpoint,
EndpointDispatcher endpointDispatcher)
{
foreach (DispatchOperation dispatch
in endpointDispatcher.DispatchRuntime.Operations)
{
ApplyDispatchBehavior(null, dispatch);
}
}
}
The last class for this custom behavior extension is an AdapterInvoker
derived from the IOperationInvoker
interface. The Invoke
method represents an interceptor for the incoming message, where we can initiate a custom behavior included in its properties:
public class AdapterInvoker : IOperationInvoker
{
object IOperationInvoker.Invoke(object instance, object[] inputs,
out object[] outputs)
{
string name = (string)_parameters["name"];
PropertyInfo pi = instance.GetType().GetProperty("Adapter");
object adapter = pi.GetValue(instance, null);
if (adapter == null && AdapterType != null)
{
pi.SetValue(instance, Activator.CreateInstance(this.AdapterType,
new object[] { this.Parameters }), null);
}
return this.innerOperationInvoker.Invoke(instance, inputs, out outputs);
}
}
Now, it is time to talk about the service. Oops, here is something that needs to be shown before we can continue further - the adapter interface. The following code snippet shows its abstract declaration:
public interface IWSTransferAdapter
{
EndpointAddress Create(object resource);
object Get(MessageHeaders resourceIdentifier);
object Put(MessageHeaders resourceIdentifier,
object resource);
void Delete(MessageHeaders resourceIdentifier);
}
As you can see, the above interface is very lightweight, actually it maps a WS-Transfer interface in the CRUD manner. The Create
method will return an EndpointAddress
of the service operation, including the resource identifier element. The operation service (adapter) knows the DataContract for the resource identifier, therefore it will be easy to find in the MessageHeaders
collection during each operation service.
Service
The WSTransferService
class is derived from the ServiceAdapterBase
and IWSTransfer
interfaces. It represents the WCF (Indigo) service layer for WS-Transfer messaging. This service layer can be used as a generic layer for the resource, via its adapter:
public class WSTransferService<ServiceAdapter>
: ServiceAdapterBase<ServiceAdapter, IWSTransferAdapter>,
IWSTransfer where ServiceAdapter : class
{
public WSTransferService()
{
}
#region IWSTransfer Members
public CreateResponse Create(CreateRequest request)
{
if (request == null)
throw new ArgumentNullException("request");
EndpointAddress resourceEndpointAddress =
this.Adapter.Create(request.Resource);
return new CreateResponse(resourceEndpointAddress);
}
public GetResponse Get(GetRequest request)
{
if (request == null)
throw new ArgumentNullException("request");
MessageHeaders resourceIdentifier =
OperationContext.Current.IncomingMessageHeaders;
object resource =
this.Adapter.Get(resourceIdentifier);
return new GetResponse(resource);
}
public PutResponse Put(PutRequest request)
{
if (request == null)
throw new ArgumentNullException("request");
MessageHeaders resourceIdentifier =
OperationContext.Current.IncomingMessageHeaders;
this.Adapter.Put(resourceIdentifier,
request.Resource);
return new PutResponse();
}
public DeleteResponse Delete(DeleteRequest request)
{
if (request == null)
throw new ArgumentNullException("request");
MessageHeaders resourceIdentifier =
OperationContext.Current.IncomingMessageHeaders;
this.Adapter.Delete(resourceIdentifier);
return new DeleteResponse();
}
#endregion
}
The purpose of the above ServiceAdapterBase
class is encapsulating and abstracting a physical adapter from the WS-Transfer service layer. It is a part of the plumbing mechanism. Note, all plumbing implementations are located in the file ServiceAdapterBehaviorExtension.cs, and it is fully reusable for service/endpoint custom extensions:
public class ServiceAdapterBase<ServiceAdapter, AdapterInterface>
where ServiceAdapter : class
{
#region Properties
private HybridDictionary _properties;
private AdapterInterface _adapter;
public AdapterInterface Adapter
{
get { return (AdapterInterface)_adapter; }
set { _adapter = value; }
}
public HybridDictionary Properties
{
get { return _properties; }
}
#endregion
public ServiceAdapterBase()
{
ServiceHost host = OperationContext.Current.Host as ServiceHost;
if (host.Description.Behaviors.Contains(typeof(AdapterAttribute)))
{
AdapterAttribute aa =
host.Description.Behaviors.Find<AdapterAttribute>();
_properties = aa.Parameters;
if (typeof(ServiceAdapter) != typeof(ConfiguredAddapter))
{
Adapter = (AdapterInterface)Activator.CreateInstance(
typeof(ServiceAdapter), new object[] { Properties });
}
else if (aa.AdapterType != null)
{
Adapter = (AdapterInterface)Activator.CreateInstance(
aa.AdapterType, new object[] { Properties });
}
else
throw new FaultException(
new ConfigurationErrorsException("Can't create Adapter"));
}
}
}
Once we have the correct adapter's instance, the WS-Transfer messaging can be passed to its processing via the IWSTransferAdapter
interface in the request/response, in a transparent manner.
Adapter boilerplate
Finally, we are at the end of message processing - Adapter
. This class must be implemented for each specific resource type and/or resource identifier. Some common logic can be decoupled in the base class and reused for another adapter, see the WSTransferServiceBase
base class in the WSTransferServiceBase.cs file. The implementation is straightforward and is easy to follow.
So, stepping into the custom adapter, let's take some practical example - MemoryStorageAdapter
. This Adapter
contains the logic for storing a resource in the memory. Before its implementation, the resource factory needs to have a DataContract
for the resource identifier. The following code snippet shows a very simple example:
[XmlSerializerFormat]
[Serializable]
[XmlRoot(ElementName = "ResourceDescriptor",
Namespace = "http://www.rkiss.net/schemas/sb/2005/06/servicebus")]
[DataContract(Name = "ResourceDescriptor",
Namespace = "http://www.rkiss.net/schemas/sb/2005/06/servicebus")]
public class ResourceDescriptor
{
#region DataMembers
string _id;
string _name;
string _key;
string _topic;
#endregion
#region Properties
[DataMember(Order=0)]
public string ResourceId
{
get { return _id; }
set { _id = value; }
}
[DataMember(Order=1)]
public string ResourceName
{
get { return _name; }
set { _name = value; }
}
[DataMember(Order=2)]
public string Key
{
get { return _key; }
set { _key = value; }
}
[DataMember(Order=3)]
public string Topic
{
get { return _topic; }
set { _topic = value; }
}
#endregion
#region Constructors
public ResourceDescriptor() { }
public ResourceDescriptor(string id)
{ ResourceId = "guid:" + id; }
#endregion
}
We have the well-known ResourceDescriptor
data contract as a representation of the resource identifier. Then, using a WSTransferAdapterBase
base class, the Adapter
class may look like the following:
class MemoryStorageAdapter
: WSTransferAdapterBase<ResourceDescriptor>,
IWSTransferAdapter
{
Hashtable _storage = null;
string strStorageName = string.Empty;
object SyncRoot = new object();
public Hashtable Storage
{
get { return _storage; }
}
public MemoryStorageAdapter(HybridDictionary config)
{
base.Properties = config;
strStorageName = base.Properties["name"] as string;
if(strStorageName == null)
throw new Exception("Missing name of the Storage");
lock (SyncRoot)
{
object storage =
AppDomain.CurrentDomain.GetData(strStorageName);
if (storage == null)
{
storage =
Hashtable.Synchronized(new Hashtable());
AppDomain.CurrentDomain.SetData(strStorageName,
storage);
}
_storage = storage as Hashtable;
}
}
public EndpointAddress Create(object resource)
{
ResourceDescriptor rd =
new ResourceDescriptor(Guid.NewGuid().ToString());
lock (SyncRoot)
{
Storage.Add(rd.ResourceId, resource);
}
return this.ResourceEndpointAddress(null, rd);
}
public object Get(MessageHeaders resourceIdentifier)
{
ResourceDescriptor rd =
GetResourceIdentifier(resourceIdentifier);
lock (SyncRoot)
{
if (Storage.Contains(rd.ResourceId) == true)
{
return Storage[rd.ResourceId];
}
}
throw new Exception(string.Format("The Resource " +
"[rid={0}] doesn't exist in the storage",
rd.ResourceId));
}
public object Put(MessageHeaders resourceIdentifier,
object resource)
{
ResourceDescriptor rd =
GetResourceIdentifier(resourceIdentifier);
if (ResourceType != null &&
resource.GetType() != ResourceType)
throw new NullReferenceException("The " +
"resource type is different from factory");
lock (SyncRoot)
{
if (Storage.Contains(rd.ResourceId) == true)
{
Storage[rd.ResourceId] = resource;
return;
}
}
throw new Exception(string.Format("The " +
"Resource [rid={0}] doesn't exist in the storage",
rd.ResourceId));
}
public void Delete(MessageHeaders resourceIdentifier)
{
ResourceDescriptor rd =
GetResourceIdentifier(resourceIdentifier);
lock (SyncRoot)
{
if (Storage.Contains(rd.ResourceId) == true)
{
Storage.Remove(rd.ResourceId);
return;
}
}
throw new Exception(string.Format("The " +
"Resource [rid={0}] doesn't exist in the storage",
rd.ResourceId));
}
}
As you can see above, the constructor creates a new storage under the well-known name in the AppDomain
data space; if it does not exist. The CRUD methods are very simple as well - making an operation on the Hashtable
storage.
Of course, the adapter implementation can have more sophisticated logic based on the resource type, factory, and operation. Any complicated resource type will not make an impact on the WSTransfer
service layer, only the Adapter
object. This is the advantage of this design pattern.
Service boilerplate
As I mentioned earlier, there are more design patterns for Level 1. Here is one more example - creating a service driven by a resource type. In this example, we do not have an adapter; the service needs to incorporate all the logic for the specific resource type. However, we can still reuse our WSTransferAdapterBase
base class for common handling of the WS-Transfer messaging. The following code snippet shows a boilerplate for this kind of WS-Transfer service. The ResourceDescriptor
is used as an example of the resource identifier:
public class xxxService
: WSTransferAdapterBase<ResourceDescriptor>, IWSTransfer
{
#region IWSTransferFactory Members
public CreateResponse Create(CreateRequest request)
{
ResourceDescriptor rd =
new ResourceDescriptor(Guid.NewGuid().ToString());
EndpointAddress resourceEndpointAddress =
base.ResourceEndpointAddress(null, rd);
return new CreateResponse(resourceEndpointAddress);
}
#endregion
#region IWSTransferOperation Members
public GetResponse Get(GetRequest request)
{
object resource = null;
MessageHeaders resourceIdentifier =
OperationContext.Current.IncomingMessageHeaders;
ResourceDescriptor rd =
GetResourceIdentifier(resourceIdentifier);
return new GetResponse(resource);
}
public PutResponse Put(PutRequest request)
{
object resource = request.Resource;
MessageHeaders resourceIdentifier =
OperationContext.Current.IncomingMessageHeaders;
ResourceDescriptor rd =
GetResourceIdentifier(resourceIdentifier);
return new PutResponse();
}
public DeleteResponse Delete(DeleteRequest request)
{
MessageHeaders resourceIdentifier =
OperationContext.Current.IncomingMessageHeaders;
ResourceDescriptor rd =
GetResourceIdentifier(resourceIdentifier);
return new DeleteResponse();
}
#endregion
}
Level 2 - consumer
The consumer side is represented by the factory and operation proxies based on the ServiceContract. The following proxies can be easily implemented programmatically, using the request
/response
core objects, if the consumer side is driven by Indigo technology. The first class, WSTransferFactoryProxy
, is responsible for sending a WS-Transfer create message; the operation proxy is created based on its response:
public class WSTransferFactoryProxy
: ProxyBase<IWSTransferFactory>, IWSTransferFactory
{
#region Constructors
public WSTransferFactoryProxy(){}
public WSTransferFactoryProxy(string configurationName)
: base(configurationName){}
public WSTransferFactoryProxy(Binding binding)
: base(binding){}
public WSTransferFactoryProxy(Binding binding,
EndpointAddress address)
: base(address, binding) { }
#endregion
#region IWSTransferFactory
public CreateResponse Create(CreateRequest request)
{
return base.Channel.Create(request);
}
#endregion
#region API (Business Methods)
public WSTransferOperationProxy Create(object resource)
{
CreateResponse response =
base.Channel.Create(new CreateRequest(resource));
return new WSTransferOperationProxy(base.Endpoint.Binding,
response.EndpointAddress);
}
#endregion
}
public class WSTransferOperationProxy :
ProxyBase<IWSTransferOperation>,
IWSTransferOperation
{
#region Constructors
public WSTransferOperationProxy(){}
public WSTransferOperationProxy(string configurationName)
: base(configurationName){}
public WSTransferOperationProxy(Binding binding,
EndpointAddress address)
: base(address, binding) { }
#endregion
#region IWSTransferOperation Members
public GetResponse Get(GetRequest request)
{
return base.Channel.Get(request);
}
public PutResponse Put(PutRequest request)
{
return base.Channel.Put(request);
}
public DeleteResponse Delete(DeleteRequest request)
{
return base.Channel.Delete(request);
}
#endregion
#region API (Business) Methods
public object Get()
{
GetResponse response = Get(new GetRequest());
return response.Resource;
}
public void Put(object resource)
{
PutResponse response =
Put(new PutRequest(resource));
}
public void Delete()
{
DeleteResponse response =
Delete(new DeleteRequest());
}
#endregion
}
The second proxy class (shown above), WSTransferOperationProxy
, is really very lightweight, it is a wrapper around the request
/response
objects.
We can see the usage of the above proxies in the following code snippet. I have bolded the code lines that are related to WS-Transfer messaging. The first step is to create an instance of WSTransferFactoryProxy
. In this example, the configuration file has been used - see the next code snippet.
The steps are very simple:
- create new factory,
- create resource,
- get resource,
- update resource,
- get updated resource,
- delete resource,
- close operation,
- close factory.
Note, that the second test example shows another kind of resource (CreateResouce
object) passing to the factory. It is based on the well-known DataContract
between the specific factory and its consumer:
using (WSTransferFactoryProxy factory =
new WSTransferFactoryProxy("LocalStorageFactory"))
{
Console.WriteLine("Test 1: Create a simply resource ");
object myResource = "urn:This_is_a_Test_1";
WSTransferOperationProxy operation =
factory.Create(myResource);
Console.WriteLine("\n Press any key to 'Get Resource'");
Console.ReadLine();
object resource = operation.Get();
Util.ShowMe(resource);
Console.WriteLine("\n Press any key to 'Put Resource'");
Console.ReadLine();
object myNewResource = Guid.NewGuid();
operation.Put(myNewResource);
Console.WriteLine("\n Press any key to 'Get Resource'");
Console.ReadLine();
resource = operation.Get();
Util.ShowMe(resource);
Console.WriteLine("\n Press any key to 'Delete Resource'");
Console.ReadLine();
operation.Delete();
Console.WriteLine("Test 2: Create/Get/Delete complex type");
CreateResource createResource = new CreateResource();
createResource.ResourceDescriptor =
new ResourceDescriptor(Guid.NewGuid().ToString());
createResource.Resource = "urn:This_is_a_Test_2";
operation = factory.Create(createResource);
resource = operation.Get();
operation.Delete();
Util.ShowMe(resource);
operation.Close();
}
Here is its configuration file. Note, that the client has two endpoints, one is for the WS-Transfer factory and the other one for the WS-Transfer operation:
<system.serviceModel>
<client>
<endpoint name="PublicStorageFactory"
behaviorConfiguration="logging"
address="net.tcp://localhost:11111/PublicStorage"
binding="customBinding"
bindingConfiguration="Binding1"
contract="RKiss.WSTransfer.IWSTransferFactory"/>
<endpoint name="PublicStorage"
behaviorConfiguration="logging"
address="net.tcp://localhost:11111/PublicStorage"
binding="customBinding"
bindingConfiguration="Binding1"
contract="RKiss.WSTransfer.IWSTransfer" />
</client>
<bindings>
<customBinding>
<binding name="Binding1">
<tcpTransport/>
</binding>
</customBinding>
</bindings>
</system.serviceModel>
OK, it is time to demonstrate a test. I have included a small WS-Transfer client and console host process. The following screen shows both these interactions. Note that the messages are published by the configurable tiny console logger based on the MessageInterceptors. I have included this "small" logger project in the download. Note that the console logger assembly must be installed in the GAC.
1. Launch the ServiceHost program:
2. Launch the client application, and check the create request/response messages. Then, press any key to see the next pair of WS-Transfer messaging:
In this article, I have described WS-Transfer messaging and its implementation using the Indigo communication model. The WS-Transfer architectural style in the connected system enables to encapsulate the business layer from the "physical" resource layer (any piece of information). The pair of services, such as factory and operation, work together based on the resource identifier, in a transparent manner. In addition, the WS-Transfer style in the application model enables creating a logical service bus with a plug and play technique. I do recommend reading more about the WS-Transfer (REST) style in the links provided in the reference section.