Archives

Archives / 2008 / October
  • REST and Workflow services play well together - Part II

    In my previous post "REST and Workflow services play well together", I mentioned that Http Cookies were one of the built-in mechanisms for transferring the workflow context across calls between the client and the service. While cookies work well for http services, from my point of view, simple Http Headers naturally fit better in a REST architecture. As consequence, I decided to extend the WCF channel stack to support a new a custom channel (Or custom binding) for transferring the workflow context as a http header.

    The configuration of this binding WebHttpContextBinding is quite straightforward, I just created a new binding "WebHttpContextBinding" that can easily configured for workflow service,

    <services>

      <service name="RestWorkflows.OrderWorkflow">

        <endpoint address="" behaviorConfiguration="MyServiceBehavior" binding="webHttpContext" bindingConfiguration="myServiceBinding" contract="RestWorkflows.IOrderService" />

      </service>

    </services>

    <bindings>

      <webHttpContext>

        <binding name="myServiceBinding"></binding>

      </webHttpContext>

    </bindings>

    <behaviors>

      <endpointBehaviors>

         <behavior name="MyServiceBehavior">

           <webHttp />

         </behavior>

      </endpointBehaviors>

    </behaviors>

    <extensions>

      <bindingExtensions>

        <add name="webHttpContext" type="Microsoft.ServiceModel.Samples.WebHttpContextBindingCollectionElement, WebHttpContext, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />

      </bindingExtensions>

    </extensions>

    The result of using this binding will be a new Http Header WscContext (encoded as Base64) being transmitted between the client and the service,

    <MessageLogTraceRecord Time="2008-10-29T14:22:59.5258021-02:00" Source="TransportSend" Type="System.ServiceModel.Channels.BodyWriterMessage" xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
       <Addressing>
         <Action></Action>
       </Addressing>
       <HttpResponse>
          <StatusCode>Created</StatusCode>
          <WebHeaders>      <WscContext>77u/PENvbnRleHQgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwNi8wNS9jb250ZXh0Ij48UHJvcGVydHkgbmFtZT0iaW5zdGFuY2VJZCI+NDZkZGY3NTktMzdjNS00ZDZlLWI2ZTItNjVmMDNjMDVmYTAyPC9Qcm9wZXJ0eT48L0NvbnRleHQ+</WscContext>
           </WebHeaders>
        </HttpResponse>
        <order xmlns="http://starbucks.example.org" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
          <cost>6</cost>
          <drink>latte</drink>
          <next xmlns:a="http://example.org/state-machine">
             <a:next>

              <a:rel>http://starbucks.example.org/payment</a:rel>
               <a:type>application/xml</a:type>
               <a:uri>http://localhost:8000/payment/order/0b756654-161b-43d0-912f-4236552dc337</a:uri>
             </a:next>
             <a:next>
                 <a:rel>http://starbucks.example/order/update</a:rel>
                 <a:type>application/xml</a:type>
                 <a:uri>http://localhost:8000/order/0b756654-161b-43d0-912f-4236552dc337</a:uri>
             </a:next>
           </next>
         </order>
    </MessageLogTraceRecord>

    <MessageLogTraceRecord Time="2008-10-29T14:22:59.5726021-02:00" Source="TransportReceive" Type="System.ServiceModel.Channels.BufferedMessage" xmlns="http://schemas.microsoft.com/2004/06/ServiceModel/Management/MessageTrace">
       <HttpRequest>
          <Method>PUT</Method>
           <QueryString></QueryString>
           <WebHeaders>
          <WscContext>77u/PENvbnRleHQgeG1sbnM9Imh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAwNi8wNS9jb250ZXh0Ij48UHJvcGVydHkgbmFtZT0iaW5zdGFuY2VJZCI+NDZkZGY3NTktMzdjNS00ZDZlLWI2ZTItNjVmMDNjMDVmYTAyPC9Qcm9wZXJ0eT48L0NvbnRleHQ+</WscContext>
            <Content-Length>566</Content-Length>
            <Content-Type>application/xml</Content-Type>
            <Expect>100-continue</Expect>
             <Host>localhost:8000</Host>
          </WebHeaders>
        </HttpRequest>
        <order xmlns="http://starbucks.example.org" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
              <cost>6</cost>
              <drink>cappuchino</drink>
              <next xmlns:a="http://example.org/state-machine">
                <a:next>
                  <a:rel>http://starbucks.example.org/payment</a:rel>
                  <a:type>application/xml</a:type>
                  <a:uri>http://localhost:8000/payment/order/0b756654-161b-43d0-912f-4236552dc337</a:uri>
                 </a:next>
                 <a:next>
                   <a:rel>http://starbucks.example/order/update</a:rel>
                   <a:type>application/xml</a:type>
                   <a:uri>http://localhost:8000/order/0b756654-161b-43d0-912f-4236552dc337</a:uri>
                 </a:next>
               </next>
           </order>
    </MessageLogTraceRecord>

    The code for the custom binding is available to download from this location.

    Read more...

  • REST and Workflow services play well together

    REST is not just about CRUD interfaces for your services, a complete long-running workflow can be modeled through the basic verbs as well. As my friend Jesus mentioned, the article "How to get a cup of coffee" represents an excellent source about this topic.

    The example I will show here is based on that article, during the course of this post I will try to illustrate all the steps required to implement a workflow as the one mentioned there using WF services. Unfortunately, no examples exist (or at least I could not find any) about how to configure the WCF web model to use the new WF services, so I decided to create a new one from scratch.

    This example only illustrates the workflow from the customer point of view, he can place an order, pay it and wait for his drink. He can also modify the order in the middle before it is paid.

     

    The interface for our REST service will looks like this:

    [ServiceContract]

    public interface IOrderService

    {

      [OperationContract]

      [WebInvoke(Method="POST", UriTemplate="order")]

      Order PlaceOrder(Order order);

     

      [OperationContract]

      [WebInvoke(Method = "PUT", UriTemplate = "order/{id}")]

      Order UpdateOrder(string id, Order order);

     

      [OperationContract]

      [WebInvoke(Method="PUT", UriTemplate="payment/order/{id}")]

      void PayOrder(string id, Payment payment);

    }

    Only three operations available, each one maps exactly with one ReceiveActivity in the workflow. For instance, the receive activity OnOrderPlaced waits for a client call to the PlaceOrder operation and then moves the workflow to the next state, which is "OrderPlaced" in this case. While the workflow is in the state "OrderPlaced", only the operations UpdateOrder and PayOrder can be executed by the client (If he try to execute PlaceOrder again, it will receive a 404 http error, "Not found").

    The OnOrderPlacedCode is an simple code activity that contains the actual operation implementation. I used a code activity for a sake of simplicity, a custom activity for creating the order could also be used there.

    The code for this activity is quite simple, most of it is hard-coded,

    private void OnOrderPlacedCode_ExecuteCode(object sender, EventArgs e)

    {

      currentOrder = new Order();

      currentOrder.OrderId = Guid.NewGuid().ToString();

      currentOrder.Cost = (receivedOrder.Drink == "latte") ? 6 : 10;

      currentOrder.Drink = receivedOrder.Drink;

      currentOrder.Next = new Next[] {

          new Next

          {

             Rel = "http://starbucks.example.org/payment",

             Uri = "http://localhost:8000/payment/order/" + currentOrder.OrderId.ToString(),

          },

          new Next

          {

             Rel = "http://starbucks.example/order/update",

             Uri = "http://localhost:8000/order/" + currentOrder.OrderId.ToString()

          }};

     

      //Persist the order in some place.....

     

      WebOperationContext.Current.OutgoingResponse.StatusCode = System.Net.HttpStatusCode.Created;

    }

    currentOrder contains the instance of the order that will be returned by the service operation whereas receivedOrder is the order sent by the client application. This was map to the operation signature in the receive activity,

     

    Once the workflow is ready to be used, all we need is to configure the service host so the client application can start consuming the operations. At this point, we will face three issues:

    1. If you want to host a WF service, a WorkflowServiceHost class has to be used in the host program. Therefore, we will not have all the automatic infrastructure configuration provided by WebServiceHost, all the configuration must be done manually. It would be great to have here a combination of both service hosts.

    2. There is not a context binding for WebHttpBinding, only BasicHttpContextBinding, NetTcpContextBinding and WsHttpContextBinding are supported. We will have to use a custom binding.

    3. A cookie must be used to transfer the context information between the client and the service, it would much better to have support for http headers here. According to this post written by Jesus, this should not be complex to do.

    <system.serviceModel>

      <serviceHostingEnvironment aspNetCompatibilityEnabled="true" />

      <services>

        <service name="RestWorkflows.OrderWorkflow">

         <endpoint address="" behaviorConfiguration="MyServiceBehavior" binding="customBinding" bindingConfiguration="myServiceBinding" contract="RestWorkflows.IOrderService" />

      </service>

    </services>

    <bindings>

    <customBinding>

      <binding name="myServiceBinding">

      <webMessageEncoding></webMessageEncoding>

      <context contextExchangeMechanism="HttpCookie" protectionLevel="None"/>

      <httpTransport manualAddressing="true"/>

      </binding>

    </customBinding>

    </bindings>

    <behaviors>

    <endpointBehaviors>

      <behavior name="MyServiceBehavior">

        <webHttp />

      </behavior>

    </endpointBehaviors>

    </behaviors>

    </system.serviceModel>

    The code for the client application looks also quite simple, you only have to remember to include the cookie with the context between calls, otherwise WF will try to create a new instance of the workflow resulting in a bad request error.

    //Lets create a new order

    Order order = new Order { Drink = "latte" };

     

    HttpWebRequest createOrderRequest = CreateRequest(new Uri("http://localhost:8000/order"), "POST", null, order);

    string context = GetContext((HttpWebResponse)createOrderRequest.GetResponse());

    order = Execute<Order>(createOrderRequest.GetResponse());

     

    Console.WriteLine("Order Cost {0}", order.Cost);

    foreach (Next next in order.Next)

    {

      Console.WriteLine("Possible next step {0}", next.Uri);

    }

     

    //Lets update the existing order....

    order.Drink = "cappuchino";

     

    Uri updateOrderUri = new Uri(order.Next.Where(n => n.Rel == "http://starbucks.example/order/update").Single().Uri);

    HttpWebRequest updateOrderRequest = CreateRequest(updateOrderUri, "PUT", context, order);

    Order updatedOrder = Execute<Order>(updateOrderRequest.GetResponse());

     

    Console.WriteLine("Order Cost {0}", updatedOrder.Cost);

    foreach (Next next in updatedOrder.Next)

    {

      Console.WriteLine("Possible next step {0}", next.Uri);

    }

     

    //Lets have our drink...

    Payment payment = new Payment { Name = "John Doe", CardNumber = "1234567", Expires = "06/08", Amount = updatedOrder.Cost.GetValueOrDefault() };

     

    Uri payOrderUri = new Uri(order.Next.Where(n => n.Rel == "http://starbucks.example.org/payment").Single().Uri);

    HttpWebRequest payOrderRequest = CreateRequest(payOrderUri, "PUT", context, payment);

     

    int statusCode = Execute(payOrderRequest.GetResponse());

     

    if (statusCode == 201)

      Console.WriteLine("Here is your drink!!!");

    else

      Console.WriteLine("Sorry, there was some error while trying to process the payment");

    As you can see in the code above, I used some helper methods to execute the operations and retrieve the response/context information. These methods only represent a few lines of code,

    static HttpWebRequest CreateRequest(Uri address, string method, string context, object contract)

    {

      HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(address);

      webRequest.ContentType = "application/xml";

      webRequest.Timeout = 30000;

      webRequest.Method = method;

      webRequest.CookieContainer = new CookieContainer();

     

      if (context != null)

      {

        Cookie cookie = new Cookie("WscContext", context, address.PathAndQuery, address.Authority);

        webRequest.CookieContainer.Add(cookie);

      }

     

      DataContractSerializer serializer = new DataContractSerializer(contract.GetType());

      using (Stream stream = webRequest.GetRequestStream())

      {

        serializer.WriteObject(stream, contract);

        stream.Flush();

      }

     

      return webRequest;

    }

     

    static string GetContext(HttpWebResponse response)

    {

      if (response.Cookies["WscContext"] != null)

      {

        return response.Cookies["WscContext"].Value;

      }

     

      return null;

    }

     

    static T Execute<T>(WebResponse response)

    {

      DataContractSerializer serializer = new DataContractSerializer(typeof(T));

      using (Stream stream = response.GetResponseStream())

      {

        return (T)serializer.ReadObject(stream);   

      }

    }

    The complete solution is available to download from here.

    Read more...

  • Conditional gets in REST

    According to the Http specification, any Http GET request should be idempotent and safe. In this context, these principles have the following meaning:

    Idempotence: One or more calls with identical requests should return the same resource representation (As long as the resource has not changed on the server between calls).

    Safety:  The only action produced by a GET request is to retrieve a resource representation, no side-effects should be evident on the service side after executing the request. An example that violates this principle could be a GET request that also updates some info in the resource.

    As long as these principles are applied correctly by a service implementation, the client will have more available mechanisms to cache the resource representation on its side. This has a very positive meaning from a scalability view point, which represents a better use of the network bandwidth and an optimization in the utilization of server resources.

    Those caching mechanisms are based on what is commonly called Http conditional gets. An Http conditional get involves the use of two special headers generated by the service, ETag and Last-Modified.

    An Etag represents an opaque value that only the server knows how to recreate, it could represent anything, but it is usually a hash representing the resource version (it can be generated hashing the whole representation content or just some parts of it, like the timestamp). On the other hand, the Last-Modified headers represents a datetime that the service can use to determine whether the resource has changed since the last time it was served. The following example illustrates the request/response messages interchanged by the client/service for a service that returns information about customers

    First request, the client does not have anything on the cache.

    GET http://foo/customers/1

    Host foo
    Accept */*
    Accept-Language en-us,en;q=0.5
    Accept-Encoding gzip,deflate
    Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
    Connection close
    Referer http://foo/
    Pragma no-cache
    Cache-Control no-cache

    Response:

    Connection close
    Date Thu, 02 Oct 2008 14:46:57 GMT
    Server Microsoft-IIS/6.0
    X-Powered-By ASP.NET
    X-AspNet-Version 2.0.50727
    Content-Encoding gzip
    Content-Length 1212
    Expires Sat, 01 Nov 2008 14:46:57 GMT
    Last-Modified Mon, 29 Sep 2008 15:40:27 GMT
    Etag a9331828c517ac5d97f93b3cfdbcc9bc

    Content-Type text/xml

    The next time the client sends a new request for the same customer, some extra information will be included in the request headers,

    GET http://foo/customers/1

    Host foo
    Accept */*
    Accept-Language en-us,en;q=0.5
    Accept-Encoding gzip,deflate
    Accept-Charset ISO-8859-1,utf-8;q=0.7,*;q=0.7
    Connection close
    Referer foo
    If-Modified-Since Mon, 29 Sep 2008 15:40:27 GMT
    If-None-Match a9331828c517ac5d97f93b3cfdbcc9bc

    With these two new headers, the service can now determine whether it has to serve the resource representation again or the client can use the cached version. If the resource has not changed according to the values in those headers (If-Modified-Since for Last-Modified and If-None-Match for Etag), the service can return an Http status code "304 Not Modified", which instructs the client to use the cached version.

    This approach is widely used nowadays by syndication, RSS or Atom, that usually use the Last-Modified/If-Modified-Since to determine which items they have to send to the client.

    Since I am a WCF fan, I will also include here some code snippets to get the value in those headers and return a "304" http status code,

    Code to get those values:

    if (WebOperationContext.IncomingRequest.Headers[HttpRequestHeader.IfNoneMatch] != null)

    {

      // Do something with etag...

    }

    else if (WebOperationContext.IncomingRequest.Headers[HttpRequestHeader.IfModifiedSince] != null)

    {

      var date = DateTime.Parse(context.IncomingRequest.Headers[HttpRequestHeader.IfModifiedSince]);

    }

     

    Code to set those values:

     

    WebOperationContext.OutgoingResponse.LastModified = // sets the last modified;

    WebOperationContext.OutgoingResponse.ETag = // sets the Etag

    Code to return an Http 304 status code:

    WebOperationContext.OutgoingResponse.StatusCode = HttpStatusCode.NotModified;

    WebOperationContext.OutgoingResponse.SuppressEntityBody = true;

    Read more...