Today I finished some additional work I did on the WCF activities at codeplex.

I added some nice features (if I may say so myself :-)) that implement automatic workflow correlation based on custom attributes applied on the operation or data contract.

If you are not familiar with the activities, take a look at codeplex and see how you can very easily implement WCF services using windows workflow.

The reason I decided I wanted this is because in many services you hand out some logical Id that someone can use later as a reference to the data or process the service encapsulates.(reservation ID, tikket number, etc) If you hand out a Guid, then in the previous implementation you needed to add the Guid to the message headers in the call to the service to make the correlation to the correct workflow instance happen. While that is a very effective, it did not feel right that a consumer of a service implemented with the activities required knowledge of those headers and the fact that the client is responsible for providing the headers just did not seem right.

So what I did is add a WCF behavior to the activities that react on the fact that you mark a parameter in the operation contract or in the data contract as a workflow correlation parameter. This way the client does not need to know the service uses workflow underneath and what is even more great I added the capability to specify your own correlation class that can translate some functional parameter to the actual workflow Guid. The implementation of that mapping is all up to you and can e.g. retrieve the Guid based on some functional data you returned in the previous calls.

The way it works is as follows:

You build a service using the WCF activities like show in picture below:

In each activity you select a method from an interface you want to implement. e.g.:

public interface IReservation
{
  int MakeReservation(ReservationRequestMsg inputMsg);
  bool UpdateReservation(int reservationID, ReservationUpdateMsg inputMsg);
  bool CancelReservation([WorkflowCorrelationParameter(typeof(LookupReservationID))]int reservationID);
}

As you can see you anotate the reservationID parameter in the cancelReservation method as the correlation parameter using the added custom attribute.

The correlation of the reservation ID to a guid specifying the correct workflow instance is done by the specified class LookupReservationID that implements the interface IWorkflowCorelationLookup

If your reservationID would be of the type Guid, you can use that without the mapping class and you would leave the attribute without any arguments. If you do that, it will just use the parameter directly to find the correct workflow instance.

If you do need the mapping then you implement the IWorkflowCorelationLookup interface in your class and you are done. A sample of such a class looks like this (note, this is a very simplistic example , you can do your looup any way you like by e.g. finding the correct workflow instance id in a database based on your parameters value)

public class LookupReservationID : IWorkflowCorelationLookup
{
  static Dictionary<int, Guid> reservationIds = new Dictionary<int, Guid>();
 
public void AddWorklfowIdForReservation(int reservationID, Guid workflowInstanceID)
  {
     reservationIds.Add(reservationID, workflowInstanceID);
  }
  public Guid LookupWorkflowIDForParameter(object parameter)
  {
     int reservationID = Convert.ToInt32(parameter);
     if (reservationIds.ContainsKey(reservationID))
     { return reservationIds[reservationID]; }
     else
       throw new InvalidOperationException(string.Format("Unable to find reservation ID
                                                                        {0}"
,reservationID));
     }
  }
}

If you now take a look at calling the service from the client your code looks like this:

Make the reservation

Reservation.ReservationClient proxy = new TestClient.Reservation.ReservationClient();
TestClient.Reservation.ReservationRequestMsg request = new
                                             TestClient.Reservation.ReservationRequestMsg();
int reservationNumber = proxy.MakeReservation(request);
proxy.Close();

Cancel the reservation

Reservation.ReservationClient proxy = new TestClient.Reservation.ReservationClient();
 // now do the call again and the reservation number is automaticly corelated to the workflow
bool updateSucceeded = proxy.CancelReservation(reservationNumber);

Now compare that to the previous implementation where you needed to do the correlation at the client yourself:

Make the reservation:

Reservation.ReservationClient proxy = new TestClient.Reservation.ReservationClient();
using (OperationContextScope scope = new OperationContextScope(proxy.InnerChannel))
{
   TestClient.Reservation.ReservationRequestMsg request = new
                                             TestClient.Reservation.ReservationRequestMsg();
   int reservationNumber = proxy.MakeReservation(request);
  // now get the header with the Workflow instance ID so we can use it for subsequent calls
   int headerIndex =
              OperationContext.Current.IncomingMessageHeaders.FindHeader("WorkflowInstanceID",
                                                                           "WCFInputActivity");
   instanceID = OperationContext.Current.IncomingMessageHeaders.GetHeader<Guid>(headerIndex);
   proxy.Close();
}

Update the reservation:

Reservation.ReservationClient proxy = new TestClient.Reservation.ReservationClient();
using (OperationContextScope scope = new OperationContextScope(proxy.InnerChannel))
{
  // now do the call again and add a message header to corelate the workflow
  MessageHeader<Guid> mhg = new MessageHeader<Guid>(instanceID);
  MessageHeader untyped = mhg.GetUntypedHeader("WorkflowInstanceID", "WCFInputActivity");
  OperationContext.Current.OutgoingMessageHeaders.Add(untyped);

  TestClient.Reservation.ReservationUpdateMsg request = new
                                               TestClient.Reservation.ReservationUpdateMsg();
  bool updateSucceeded = proxy.UpdateReservation(reservationNumber, request);
  proxy.Close();
}

 

As you can see the new implementation is very clean and no code is required anymore that relates to the service implementation using workflow.

I hope you guys like this behavior and that it help you implement better services using workflow.

b.t.w. let me know what you think or if you run into any issues let me know and I will fix them a.s.a.p.

Enjoy.

— Marcel —

CTO at Xpirit, Microsoft Regional Director, Visual studio ALM MVP, Speaker, Pluralsight Author and IT Architect Consultant