Harin Sandhoo's Blog

May 26, 2010

SharePoint 2010 Pluggable Workflow Services with Correlation and External Callbacks

Filed under: SharePoint, Workflow — Harin @ 5:35 pm

Paul Andrew recently wrote an article on MSDN outlining Pluggable Workflow Services. This blog posting extends that example to explain how to include correlation tokens as well as an external callback mechanism. The point of including correlation tokens is if you want to let multiple events of the same type wait for an external event. The point of adding a custom callback mechanism would be if you wanted to have a web part, service application, or custom web service that allows users to progress workflows directly instead of modifying a task list item. For example, if you wanted to provide users with the ability to progress the workflow just by sending an email, you could use this mechanism. Wouter van Vugt has a good Channel 9 video explaining most of these concepts which I’d recommend watching as well.

Correlation Tokens

If you’ve ever created a workflow in SharePoint, then you’ve used correlation tokens with almost every SharePoint related activity, such as CreateTask. So why is correlation needed in your custom local communication service definition? Basically it is a mechanism that allows the workflow runtime to route inbound messages to the correct instance of the persisted activities, if you have more than one activity that is waiting for external communication events. If you are writing a pluggable workflow service and have multiple HandleExternalEvent activities of the same interface type and operation, then you need to provide correlation token capabilities. If you don’t have more than one activity instance in the same workflow persisted and waiting on a particular event, then you don’t have to worry about correlation tokens. Here’s how to add them.

Add a parameter to the ExternalDataEventArgs

This parameter is used to pass the correlation token id between the host and the client.  

   1: [Serializable]

   2: public class PrimeCalculatorEventArgs : ExternalDataEventArgs

   3: {

   4:     public PrimeCalculatorEventArgs(Guid id, string tokenId) : base(id)

   5:     {

   6:         this.TokenID = tokenId;

   7:     }

   8:

   9:     public string Answer;

  10:     public string TokenID;

  11: }

Modify the ExternalDataExchange interface definition

There are a number of things you’ll need to do here.

  1. Annotate the interface with a CorrelationParameterAttribute. In this you specify the token parameter name as defined in the interface method signature.
  2. Mark each outbound method that is in the CallExternalMethod and HandleExternalEvent pair with a CorrelationInitializerAttribute. The parameters of the outbound methods need to have the token parameter name specified in Step 1.
  3. Once that’s done, you need to mark the inbound method with CorrelationAliasAttribute. The first parameter in the CorrelationAliasAttribute attribute needs to match the token parameter name in Step 1. The second parameter in the CorrelationAliasAttribute needs to match the public property specified in the ExternalDataEventArgs (see above section). Note, with the second parameter the first part (the “e”) doesn’t really matter AFAIK, it’s really the second part of the parameter (the TokenID) that needs to match.
   1: [ExternalDataExchange]

   2: [CorrelationParameter("tokenId")]  // Step 1

   3: public interface IPrimeCalculatorService

   4: {

   5:     [CorrelationInitializer()]  // Step 2

   6:     void CalculatePrimes(string tokenId, int topNumberExclusive);

   7:

   8:     [CorrelationInitializer()]  // Step 2

   9:     void RequestValidation(string tokenId);

  10:

  11:

  12:     [CorrelationAlias("tokenId","e.TokenID")]  // Step 3

  13:     event EventHandler<PrimeCalculatorEventArgs> GetAnswer;

  14:

  15:     [CorrelationAlias("tokenId", "e.TokenID")]  // Step 3

  16:     event EventHandler<PrimeCalculatorEventArgs> ValidateAnswer;

  17: }

Implement your SPWorkflowExternalDataExchangeService

The main differences related to correlation here are that you need to pass along the token id. In CallEventHandler, when you invoke your event handler, the token id is passed along with the ExternalDataEventArgs. This is used internally by the workflow runtime to route the call to the correct activity instance in your workflow.

 

   1: /// <summary>

   2: /// Implementation of ExternalDataExchangeService / Local Communication Service

   3: /// </summary>

   4: class PrimeCalculatorService: SPWorkflowExternalDataExchangeService, IPrimeCalculatorService

   5: {

   6:

   7:     public const string SubscriptionsListName = "SubscriptionsList";

   8:

   9:     #region IPrimeCalculatorService Members

  10:

  11:     public void CalculatePrimes(string tokenId, int topNumberExclusive)

  12:     {

  13:         ThreadPool.QueueUserWorkItem(

  14:             state =>

  15:             {

  16:                 FactoringState factoringState = state as FactoringState;

  17:                 PrimeCalculatorWS.Calculate ws = new PrimeCalculatorWS.Calculate();

  18:                 string answer = ws.CalculatePrimes(factoringState.topNumberExclusive);

  19:

  20:                 SPWorkflowExternalDataExchangeService.RaiseEvent(

  21:                     factoringState.web,

  22:                     factoringState.instanceId,

  23:                     typeof(IPrimeCalculatorService),

  24:                     "GetAnswer",

  25:                     new object[] { tokenId, answer }  // NOTE:  tokenId is passed along as a parameter for the callback.

  26:                 );

  27:

  28:             },

  29:             new FactoringState(WorkflowEnvironment.WorkflowInstanceId, this.CurrentWorkflow.ParentWeb, topNumberExclusive)

  30:         );

  31:     }

  32:

  33:     public void RequestValidation(string tokenId)

  34:     {

  35:     }

  36:

  37:     public event EventHandler<PrimeCalculatorEventArgs> GetAnswer;

  38:     public event EventHandler<PrimeCalculatorEventArgs> ValidateAnswer;

  39:

  40:     #endregion

  41:

  42:     #region SPWorkflowExternalDataExchangeService Overrides

  43:

  44:     public override void CallEventHandler(Type eventType, string eventName, object[] eventData, SPWorkflow workflow, string identity, IPendingWork workHandler, object workItem)

  45:     {

  46:         string tokenId = (string)eventData[0];  // NOTE:  tokenId is extracted from eventData and passed along with the ExternalDataEventArgs

  47:         var msg = new PrimeCalculatorEventArgs(workflow.InstanceId, tokenId);

  48:         msg.Answer = eventData[1].ToString();

  49:         msg.WorkHandler = workHandler;

  50:         msg.WorkItem = workItem;

  51:         msg.Identity = identity;

  52:

  53:         if (eventName == "GetAnswer" && GetAnswer != null)

  54:         {

  55:             this.GetAnswer(null, msg);

  56:         }

  57:         else if (eventName == "ValidateAnswer" && ValidateAnswer != null)

  58:         {

  59:             this.ValidateAnswer(null, msg);

  60:         }

  61:     }

  62:

  63:

  64:     #endregion

  65: }

  66:

  67: /// <summary>

  68: /// This is a utility class to store information needed by the queued thread work item.  

  69: /// This isn't relevant to correlation directly, just used by the example and included for completeness.

  70: /// </summary>

  71: class FactoringState

  72: {

  73:     public Guid instanceId;

  74:     public SPWeb web;

  75:     public int topNumberExclusive;

  76:

  77:     public FactoringState(Guid instanceId, SPWeb web, int topNumberExclusive)

  78:     {

  79:         this.instanceId = instanceId;

  80:         this.web = web;

  81:         this.topNumberExclusive = topNumberExclusive;

  82:     }

  83: }

Generate strongly typed CallExternalMethod and HandleExternalEvent activities using WCA.exe

EDIT:  I neglected to mention that the tool was called wca.exe.  It’s used to generate the CallExternalMethod and HandleExternalEvent activities that are specific to your interface.  Here is an msdn page describing the tool.

This step is optional, but generally a good idea. The tool creates activities for that are strongly typed to your interface definition.

It is located at: C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\Bin

Add your Workflow Service / Local Communication Service to the web.config

Here is a sample snippet from Paul Andrew’s msdn article mentioned above:

<WorkflowServices><WorkflowService Assembly=”WorkflowProject1, Version=1.0.0.0, Culture=neutral, PublicKeyToken=YOURPUBLICKEY”>
</WorkflowService></WorkflowServices>

Create a workflow and start using your correlated activites!

External Callback Mechanism

This section tries to explain how you can create an external callback mechanism that doesn’t live directly within the workflow. Examples would be a web part, custom field type, custom service application, SharePoint hosted WCF service, etc, etc. To accomplish this, you first need to make a few small modifications to your SPWorkflowExternalDataExchange service. The next step is that you need write some code to call your call backs, in this example I’ll use a web part, but it can be anything that is executed within the context of a SharePoint process. The italicized point is key. AFAIK, you can’t create a standalone application to progress the workflows since the SharePoint workflow runtime isn’t loaded in those processes. Ok, so let’s get started. In the example above there was a RequestValidation and ValidateAnswer event handler I defined in my service contract. This section uses them as defined above.

SPWorkflowExternalDataExchange modifications

SPWorkflowExternalDataExchange defines two methods called CreateSubscription and DeleteSubscription. These methods are called by the workflow runtime when you use CallExternalMethod and HandleExternalEvent activities. They allow us to write information about our workflow, the Subscription ID, Workflow ID, and any correlation parameters, to some sort of persistence store so we can access it externally when we want to progress our workflows. Note: the example here was taken from Wouter van Vugt’s Channel 9 video. In this example, I’m just writing the information to a SharePoint list, but you can write it to a database / any other persistance store you like. Just keep in mind that there are fidelity concerns that come in to play here, so a SharePoint list may not be the best place if you need high performance / have lots of workflows.

   1: /// <summary>

   2: /// This method's goal is to just persist the subscription.CorrelationProperties to a list.

   3: /// NOTE:  This method uses linq to sharepoint where I to defined my own DataContext using sqlmetal (not shown here).

   4: /// This is called automatically by the workflow runtime.

   5: /// </summary>

   6: /// <param name="subscription"></param>

   7: public override void CreateSubscription(MessageEventSubscription subscription)

   8: {

   9:     EnsureSubscriptionsList(base.CurrentWorkflow.ParentWeb);

  10:

  11:     CorrelationProperty correlationProperty = subscription.CorrelationProperties.Where(p => p.Name != string.Empty).First();

  12:     using (DataContext context = new DataContext(base.CurrentWorkflow.ParentWeb.Url))

  13:     {

  14:         EntityList<SubscriptionsListItem> subscriptions = context.GetList<SubscriptionsListItem>(SubscriptionsListName);

  15:         SubscriptionsListItem entity = new SubscriptionsListItem()

  16:         {

  17:             SubscriptionId = subscription.SubscriptionId.ToString(),

  18:             WorkflowId = subscription.WorkflowInstanceId.ToString(),

  19:             CustomId = correlationProperty.Value !=null ? correlationProperty.Value.ToString() : string.Empty

  20:         };

  21:         subscriptions.InsertOnSubmit(entity);

  22:         context.SubmitChanges();

  23:     }

  24: }

  25:

  26: /// <summary>

  27: /// This method's goal is to delete the SharePoint list item when we’re done with it.  

  28: /// NOTE:  This method uses linq to sharepoint where I to defined my own DataContext using sqlmetal (not shown here).

  29: /// This is called automatically by the workflow runtime.

  30: /// </summary>

  31: /// <param name="subscriptionId"></param>

  32: public override void DeleteSubscription(Guid subscriptionId)

  33: {

  34:

  35:     using (DataContext context = new DataContext(base.CurrentWorkflow.ParentWeb.Url))

  36:     {

  37:         EntityList<SubscriptionsListItem> subscriptions = context.GetList<SubscriptionsListItem>(SubscriptionsListName);

  38:         SubscriptionsListItem entity = subscriptions

  39:             .Where(s => s.SubscriptionId == subscriptionId.ToString())

  40:             .FirstOrDefault();

  41:         if (entity != null)

  42:         {

  43:             subscriptions.DeleteOnSubmit(entity);

  44:             context.SubmitChanges();

  45:         }

  46:     }

  47: }

  48:

  49: /// Helper method to create the SharePoint list.

  50: private void EnsureSubscriptionsList(SPWeb web)

  51: {

  52:     SPList list = web.Lists.TryGetList(SubscriptionsListName);

  53:     if (list == null)

  54:     {

  55:         Guid listId = web.Lists.Add(SubscriptionsListName, "", SPListTemplateType.GenericList);

  56:         list = web.Lists.GetList(listId, true);

  57:         string subscriptionIDFieldName = list.Fields.Add("SubscriptionId", SPFieldType.Text, true);

  58:         string workflowIDFieldName = list.Fields.Add("WorkflowId", SPFieldType.Text, true);

  59:         string customIDFieldName = list.Fields.Add("CustomId", SPFieldType.Text, true);

  60:         list.Update();

  61:

  62:         SPView view = list.DefaultView;

  63:         view.ViewFields.Add(subscriptionIDFieldName);

  64:         view.ViewFields.Add(workflowIDFieldName);

  65:         view.ViewFields.Add(customIDFieldName);

  66:         view.Update();

  67:     }

  68:

  69: }

Implement your web part / custom code to progress the workflow

The key method here is SPWorkflowExternalDataExchangeService.RaiseEvent. This calls in to the workflow as we did above, passing in the workflow instance ID and the correlation token id in the ExternalDataEventArgs if you are using correlation. These parameters can be found wherever we persisted them in the CreateSubscription method defined above. In this example, that information was persisted to a list. A more robust example could look up the correct parameter ids, but this one requires users to enter them manually.

   1: [ToolboxItemAttribute(false)]

   2: public class WebPart1 : WebPart

   3: {

   4:     protected TextBox subId;

   5:     protected TextBox wfInstId;

   6:     protected TextBox corrTokenId;

   7:     protected Button validate;

   8:     protected Button invalidate;

   9:

  10:     protected override void CreateChildControls()

  11:     {

  12:         subId = new TextBox();

  13:         wfInstId = new TextBox();

  14:         corrTokenId = new TextBox();

  15:         validate = new Button()

  16:         {

  17:             Text = "Valid"

  18:         };

  19:

  20:         validate.Click += new EventHandler((s, e) =>

  21:         {

  22:             SPWorkflowExternalDataExchangeService.RaiseEvent(

  23:                                    SPContext.Current.Web,

  24:                                    new Guid(wfInstId.Text),

  25:                                    typeof(IPrimeCalculatorService),

  26:                                    "ValidateAnswer",

  27:                                    new object[] { corrTokenId.Text, "Valid answer." }

  28:                            );

  29:

  30:         });

  31:

  32:

  33:         invalidate = new Button()

  34:         {

  35:             Text = "Invalid"

  36:         };

  37:

  38:         invalidate.Click += new EventHandler((s, e) =>

  39:         {

  40:             SPWorkflowExternalDataExchangeService.RaiseEvent(

  41:                                    SPContext.Current.Web,

  42:                                    new Guid(wfInstId.Text),

  43:                                    typeof(IPrimeCalculatorService),

  44:                                    "ValidateAnswer",

  45:                                    new object[] { corrTokenId.Text, "In valid answer." }

  46:                            );

  47:

  48:         });

  49:

  50:         Controls.Add(subId);

  51:         Controls.Add(wfInstId);

  52:         Controls.Add(corrTokenId);

  53:         Controls.Add(validate);

  54:         Controls.Add(invalidate);

  55:

  56:

  57:     }

  58:

  59: }

Deploy, create your workflow, and use your web part to progress your workflows
That’s all there is to it! Hope this helps!

Blog at WordPress.com.