Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WS-Transfer for WCF (Indigo)

0.00/5 (No votes)
11 Jul 2006 1  
The article describes the design, implementation, and usage of WS-Transfer for Indigo-driven applications.

Contents

Note: This article was written using .NET Framework 3.0 - June 2006 CTP.

Introduction

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:

<?xml version="1.0" encoding="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:

<?xml version="1.0" encoding="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.

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.

Concept and design implementation

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)

  • Client

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.

ServiceContract

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);
 }

Message contract - request/response messages

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");

       // Adapter action
       EndpointAddress resourceEndpointAddress =
                 this.Adapter.Create(request.Resource);

       // done
       return new CreateResponse(resourceEndpointAddress);
    }

    public GetResponse Get(GetRequest request)
    {
       if (request == null)
         throw new ArgumentNullException("request");

       MessageHeaders resourceIdentifier =
          OperationContext.Current.IncomingMessageHeaders;

       // Adapter action
       object resource =
              this.Adapter.Get(resourceIdentifier);

       // done
       return new GetResponse(resource);
    }

    public PutResponse Put(PutRequest request)
    {

     if (request == null)
         throw new ArgumentNullException("request");

       MessageHeaders resourceIdentifier =
          OperationContext.Current.IncomingMessageHeaders;

       // Adapter action
       this.Adapter.Put(resourceIdentifier,
                                request.Resource);

       // done
       return new PutResponse();
    }

    public DeleteResponse Delete(DeleteRequest request)
    {
       if (request == null)
         throw new ArgumentNullException("request");

       MessageHeaders resourceIdentifier =
          OperationContext.Current.IncomingMessageHeaders;
         
       // Adapter action
       this.Adapter.Delete(resourceIdentifier);

       // done
       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))
            {
                // tightly coupled adapter (compiler)
                Adapter = (AdapterInterface)Activator.CreateInstance(
                      typeof(ServiceAdapter), new object[] { Properties });
            }
            else if (aa.AdapterType != null)
            {
                // loosely coupled adapter (config file)
                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());

       // action
       lock (SyncRoot)
       {
          Storage.Add(rd.ResourceId, resource);
       }

       return this.ResourceEndpointAddress(null, rd);
    }

    public object Get(MessageHeaders resourceIdentifier)
    {
       ResourceDescriptor rd =
           GetResourceIdentifier(resourceIdentifier);

       // action
       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");

       // action
       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);

       // action
       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)
    {
       // create descriptor
       ResourceDescriptor rd =
         new ResourceDescriptor(Guid.NewGuid().ToString());

       // todo: store the resource

       // add descriptor to the address
       EndpointAddress resourceEndpointAddress =
               base.ResourceEndpointAddress(null, rd);

       // done
       return new CreateResponse(resourceEndpointAddress);
    }
    #endregion

    #region IWSTransferOperation Members
    public GetResponse Get(GetRequest request)
    {
       // resource body
       object resource = null;

       // get resource identifier
       MessageHeaders resourceIdentifier =
          OperationContext.Current.IncomingMessageHeaders;

       // get descriptor
       ResourceDescriptor rd =
          GetResourceIdentifier(resourceIdentifier);

       // todo: get resource state

       // done
       return new GetResponse(resource);
    }

    public PutResponse Put(PutRequest request)
    {
       // resource body
       object resource = request.Resource;

       // get resource identifier
       MessageHeaders resourceIdentifier =
          OperationContext.Current.IncomingMessageHeaders;

       // get descriptor
       ResourceDescriptor rd =
          GetResourceIdentifier(resourceIdentifier);

       // todo: update resource state

       // done
       return new PutResponse();
    }

    public DeleteResponse Delete(DeleteRequest request)
    {
       // get resource identifier
       MessageHeaders resourceIdentifier =
          OperationContext.Current.IncomingMessageHeaders;

       // get descriptor
       ResourceDescriptor rd =
          GetResourceIdentifier(resourceIdentifier);
                 
       // todo: delete resource

       // done
       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:

/// <summary>Factory Proxy to the WSTransfer Service
/// </summary>
 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
 }

 /// <summary>Operation Proxy to the WSTransfer Service
 /// </summary>
 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>

Test

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:

Conclusion

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.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here