Durable Functions are an extension of Azure Functions that lets you write stateful functions in a serverless environment. The extension manages state, checkpoints, and restarts for you. While Microsoft Flow and Azure Logic Apps allow you to create workflows in a visual environment, Durable Functions allow you to create stateful, long-running workflows in C#.
This document, and the accompanying code samples, show how you can use Azure Durable Functions to implement SharePoint workflows. For this sample, we have a document library called Drafts and a document library called Published. An approval workflow will be created using Azure Durable Functions so that once a document is approved it will be copied from the Drafts library to the Published library. The Drafts library has a column of type person or group called DocumentOwner and a column of type person or group call Stakeholders. The workflow/function will be created such that the DocumentOwner must first approve the document. If the DocumentOwner Approves, then tasks will be created for Stakeholders to approve. The stakeholders have 1 minute to approve the tasks. If they do not approve in that timeframe, the document is automatically approved. If any stakeholder rejects the document, the workflow stops and the document is not copied to the published library.
This solution consists of an Azure Durable Function Application (https://github.com/russgove/DurableFunctionsDemo) and an SPFX project (https://github.com/russgove/DurableFunctionsDemoSPFX) that has a List View Command Set that lets the user initiate a workflow, and a webpart that lets the user see the status of all workflows and optionally terminate them
The Durable Function Application
The Durable Function application consists of two classes. The Orchestrator Class contains the functions necessary to initiate and run the workflow itself, the TaskNotifications Class contains a SharePoint webhook and the functions necessary to send events to the Orchestrator class signaling that a task has been approved or rejected,
The Orchestrator Class
The Orchestrator class in the Durable functions project has the following methods:
ApprovalStart: This is function is triggered by an HTTP Request from the SPFX List view Extension. The extension passes the Item ID that an approval has been requested on, and the email address of the person requesting the approval into this function. The function also receives a DurableOrchestrationClient from the Azure runtime which allows it to send messages to Durable functions and initiate ‘Orchestrations’. This function just reads the input passed into it and starts the ‘Publish” orchestration passing it the item ID and initiator email.
Publish: The Publish function is the core of the workflow in that it ‘orchestarates’ the activities of all the other functions. It receives a DurableOrchestrationContext parameter from the runtime which allows it to initiate Activity functions and to wait for external events (among other things).
The first thing the Publish function does is call the GetListitemData Activity Function to get the information about the document being approved. It passes in the parameters it received from the HTTP Trigger that initiated it, which contains the item ID. Note that the Publish function (the Orchestrator, in Durable functions
terminology ) does not actually call
GetListitemData (the Activity Function in Durable function terminology). It instead calls the CallActivityAsync method of the DurableOrchestrationContext which writes an entry to a queue which in turn triggers the GetListitemData function. The Publish function then stops. When GetListitemData completes and returns the ListItemData, another message is written to a queue which triggers re-execution of the Publish function with the ListItemData. This is more clearly explained in the durable functions documentation here.
After receiving the ListItemData, the Publish function triggers the ScheduleDocOwnerApproval function. It needs to pass that function the InstanceID of the currently running workflow (so we can restart the workflow after the DocumentOwner approves/rejects) and the numeric ID of the DocumentOwner (so we can assign the task to that person). Activity functions can take only a single parameter (because of the way that they are invoked via a queue) so we create a class called ScheduleDocOwnerApprovalParms so that we can pass these two values to the function in a single parameter. Note that the value passed to an activity function and the return values from an Activity function must be JSON Serializable. You cannot reliably pass or return a SharePoint List Item from an Activity function as it is not fully serializable to JSON.
The ScheduleDocOwnerApproval function will create an item in the Tasks list for the DocumentOwner to approve. When the Document Owner approves or Rejects the task, a event notification will be sent to the Approval workflow signaling that action,
The Publish function then waits for either DocOwnerApproved or DocOwnerRejected external event notification. As noted above, these events are signaled when the DocumentOwner Approves or rejects the task.
If the DocumentOwner Rejects the task, the publish function receives a
DocOwnerRejected event and triggers the execution of the DocownerRejected function to send an email, and the Publish function is completed.
If the DocumentOwner Approves the task, the Publish function receives a
DocOwnerApproved event which causes it to trigger execution of ScheduleStakeholderApproval for each of the stakeholders, and create an external event for that particular stakeholder (the event name is StakeholderApproval: plus the ID of the stakeholder). After all the stakeholder tasks and events have been created, it creates a single task (stakeHolderApprovalTask) that will be triggered once ALL StakeholderApproval events have been triggered. It also creates a stakeHolderRejectionTask that will be triggered when ANY stakeholder rejects his task,
So at this point the Publish function has tasks that it can wait on (stakeHolderApprovalTask and stakeHolderRejectionTask). We need to create an additional task that will fire one minute. This is done by creating a CancellationTokenSource (ctx) and calling the DurableOrchestrationContext createTimer method passing in the duration we want to wait for and the token of the CancellationTokenSource we created. We assign this task to a variable timeouttask.
Now the Publish function waits for any if these three tasks to complete. Once any of the three tasks has completed , the function continues on and determines which task did actually complete. If the rejection task completed, the function does nothing and terminates, Otherwise, the
stakeHolderApprovalTask or the timeout task were completed and the function copies the document to the Published library,
GetListitemData: The GetListitemData Activity function uses OfficeDevPnP.Core.AuthenticationManager to connect to the SharePoint site using the SiteUrl and credentials stored in the function’s configuration (local.settings.json if running locally or the App Settings if running in Azure).
It then fetches the Item from the Draft library using the Item ID passed in from the Publish function, and extracts the numeric DocumentOwner ID and the numeric ID’s of the stakeholders. It then creates a new ListItemData object to return these values to the Publish function. Again, parameters passed to, and return values from, an Activity Functions must be JSON Serializable. If we try to return the native SharePoint List Item the people fields will not be serialized correctly and will not be accessible in the Publish function.
ScheduleDocOwnerApproval: This Activity function creates a task in the Tasks list setting the AssignedTo and the workFlowId passed in from the Publish Function, It also sets the Action field in the task list to “DocOwnerApproval” so that the webhook can send the correct notifications to the Publish function.
DocOwnerRejected. This Activity function is called by the Publish function when the DocumentOwner Rejects a task, It simply sends out an email. It has no effect on the workflow and is included only for demonstration,
ScheduleStakeholderApproval: This Activity function works the same as the ScheduleDocOwnerApproval function—it creates a task in the Tasks list setting the AssignedTo to the stakeholder and the WorkFlowId passed in from the Publish Function. It also sets the Action field in the task list to “StakeholderApproval” so that the webhook can send the correct notifications to the Publish function.
GetAllStatus: This function is called by the ManageFunctionInstances webpart to get a list of all running workflows.
TerminateInstance: This function is called by the ManageFunctionInstances webpart to terminate a running workflow.
The TaskNotifications Class
The TaskNotifications class in the Durable functions project implements a SharePoint a webhook in Azure functions that listens for updates to items in the Tasks list.
The ReceiveApproval method will get called by SharePoint when items in our Tasks list are changed. It will call the ProcessChanges method for each notification. Note that this function also receives a DurableOrchestrationClient as a parameter so that it can send events to the orchestrator.
When an item changes the ProcesssChanges method checks to see that the task has at least two versions (versioning must be enabled on the task list for this application to work) and that the approval status has changed between the last two versions. If the item has been deleted or there are not two versions or if the status has not changed, it does nothing,
If the Action field in the task list is “DocOwnerApproval”, its sends a “DocOwnerApproved” or “DocOwnerRejected” (depending on the status) event to the instance of the Publish workflow using the RaiseEventAsync method of the DurableOrchestrationClient, causing the Publish workflow to continue.
If the Action field in the task list is “StakeholderApproval”, and the status is “Rejected” it sends a “StakeHolderRejected” event to the instance of the Publish workflow using the RaiseEventAsync method of the DurableOrchestrationClient. (The publish workflow will continue when it receives a single StakeHolderRejected event.
If the Action field in the task list is “StakeholderApproval”, its and the status is “Approved” it sends a “StakeHolderApproved:xx” (where xx is the numeric id of the stakeholder) event to the instance of the Publish workflow using the RaiseEventAsync method (The publish workflow will continue when it receives a StakeholderApproval-xx events from ALL stakeholders).
The SPFX Application
The SPFX application contains a List View Command Set with a single command ‘Start Approval’ that is enabled when a single item is selected in the Draft Library. The command does an HTTP POST to the
ApprovalStart endpoint in the Orhestartor class of the durable function to initiate the workflow.
The SPFX application also contains a webpart ( ManageFunctionInstances) that lets the user see all instances of the workflow . It does this by calling the GetAllStatus endpoint in the orchestrator class and displaying the results in a DetailsList. The webpart also has a single action in the Command Bar to Terminate a selected instance.
Installing and Testing the sample
Create a new site
Run the following PNP-PowerShell command to create a new modern site:
Connect-PnPOnline -Url https://yourtenant.sharepoint.com/ New-PnPTenantSite ` -Title "Test Durable Functions" ` -Url "https://yourtenant.sharepoint.com/sites/DurableFunctionsDemo" -Description "Test Durable Functions" ` -Owner "firstname.lastname@example.org" ` -Lcid 1033 ` -Template "STS#3" ` -TimeZone 0 ` -Wait ` -StorageQuota 900000
Run the script CreateListsForDurableFunctionsSample.ps1 in the DurableFunctionsDemo application to create the required lists and libraries.
Navigate to https://tenant.sharepoint.com/sites/DurableFunctionDemo/_layouts/15/appregnew.aspx to create a new App Registration.
Generate a new Client ID and Client Secret and complete the form as follows:
Update the local.setting.json of the Durable Functions project with the clientID and ClientSecret you just created. Also add the siteUrl, Allowed Origins and the ID of the task list that was created previously.
Now navigate to https://tenant.sharepoint.com/sites/DurableFunctionDemo/_layouts/15/appinv.aspx to grant permissions to the id. Enter the App ID and click lookup. Then enter the following for the Permission Request XML:
<AppPermissionRequests AllowAppOnlyPolicy="true"> <AppPermissionRequest Scope="http://sharepoint/content/sitecollection/web" Right="Write" /> </AppPermissionRequests>
Next we need to add the webhook. To do so the Azure Function app needs to be running. So start debugging and wait for it to complete. You should see text like this indicating it has started:
Next open a command prompt window and change directory to the ngrok folder in Program files and enter the command:
ngrok http 7071 –host-header=localhost:7071
You should get a message like this:
This means that all requests to https://0a656ad1.ngrok.io will be routed to your local machine on port 7071.
With that, we can now add a webhook to our ‘Tasks’ list using the following PNP PowerShell commands:
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12;
Connect-PNPOnline “https://yourtenant.SharePoint.com/sites/DurableFunctionDemo” -Credentials o365
Add-PnPWebhookSubscription -List Tasks -NotificationUrl https://0a656ad1.ngrok.io/api/ReceiveApproval
Now switch to the SPFX project in VSCode. Edit the serve.json file and under serverConfigurations change the default pageUrl to your tenant. Now debug the project. When the broswer starts, allow it to run the Debug Scripts. Open a new tab in the browser and point it to the _layouts/15/workbench.aspx of the site you are testing in and add the ManageWorkflowInstances webpart.
Switch back to the browser tab that has the Draft Library open and upload a file. Edit the properties and add a Document owner and a few Document Approvers (can be anyone). Select the file and then choose the ‘Start Approval’ item from the menu. This will post a message to the ApprovalStart function in the Orchestration class of your Visual Studio project. After a few seconds the ApprovalStart function will initiate a new “Publish” orchestration for the document you selected.
Switch browser tabs to the tab containing the workbench and refresh the page. You should see a single instance of your workflow running.
Open another browser tab and navigate to the Tasks list in your site, There should be a single task there assigned to whomever you specified as the DocumentOwner. The workFlowId on this task should match the InstanceID of the workflow in the ManageWorkflowInstances webpart in the Workbench tab.
Go ahead and mark this task as approved. This will cause SharePoint to fire the webhook and the webhook will see that a DocOwnerApproval task was received and fire the DocOwnerApproval event on the Publish function. The publish function will now create tasks for each of the Stakeholders. Approve these tasks and the webhook will run for each change, notifying the Publish function of the approvals. If all the approvals are completed within the allotted one minute the file will be copied to the published Library
Azure Durable Functions can be used to implement complex business logic in code.
An additional advantage to using Azure Durable Functions is that the workflow is reuseable! You just need to grant the Azure function Write Permission to any additional sites, and add the List View Command Set and webhook to any additional lists/libraries you want the workflow to be available on.