Tuesday, 30 January 2018

Use Flow HTTP Webhook to call Azure Function - Send notification email after PnP provisioning

When using Site Designs and PnP for applying provisioning templates to sites, the order of events is something like this:

  1. When a Communication site or an Office 365 Group connected Team site is created, a Site Design gets applied to it.
  2. The Site Design contains a triggerFlow action which starts a Microsoft Flow (configured to be triggered by an http request).
  3. The flow adds an item to an Azure storage queue which triggers and Azure Function.
  4. The Azure Function contains the PnP template and the code to apply the template to the newly created site.

This approach is described in the guidance here: Calling the PnP provisioning engine from a site script

Now the problem with this sequence of actions is that (as of this time) they are all Asynchronous events i.e. "fire and forget". When the Site Design executes the triggerFlow action, it does not wait for the Flow to complete before showing the success screen to the user. Similarly, after the Flow adds an item to the storage queue, quite understandably, it does not wait for the triggered Azure Function to finish executing before completing its run.

In this sequence of async events, if we want to perform an action like sending a notification email after the provisioning is complete, we have the following options:


Multiple Flows:

Use an output storage queue and in the Azure Function, add an item to it when the provisioning completes. Have another Flow triggered by the output queue, which sends the email. I am not a big fan of this approach as this means managing an output queue as well as a second Flow just for sending the email notification.


Polling:

If we want to avoid creating a second Flow, another option is to use a Do Until action and poll for when messages arrive on the queue. The main drawback of this approach is that we must keep track of different instances of the Flow. If multiple users are creating sites and there are multiple messages arriving on the queue, we want to send the email only after the message of our own site arrives. This is doable using the Flow instance id but a bit too complex for my liking :)

The solution: HTTP Webhook

Fortunately, there is a nice way to handle this. Using the HTTP Webhook action, we can basically wait for the provisioning to complete before returning the control back to the Flow. Since we are in a serverless world here, the Flow will essentially pause here and "wake up" when we want it to.

To use this approach, we have to make a slight modification to the approach suggested in the Microsoft documentation. In this approach, instead of firing the Azure Function with a queue trigger, we will fire it using the HTTP Webhook action. The Function will be configured to execute on HTTP trigger i.e. when an HTTP request is made to an endpoint.

Here is an overview of actions for the Flow:


The Flow starts when the Site Design executes the triggerFlow action:


Now this is where the magic happens. The HTTP Webhook action makes a request to the Azure Function. It passes the call back url as a parameter and then waits. Whenever the Azure Function finishes executing, it is expected to make a request to the call back url. When a request will be received in the call back url, the HTTP Webhook action will finish and the Flow will resume with the data passed back from the Azure Function.


Here is the code for the Azure Function:

public static class ApplyPnPTemplateHTTPTrigger
{
private static HttpClient httpClient = new HttpClient();
[FunctionName("ApplyPnPTemplateHTTPTrigger")]
public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log, ExecutionContext executionContext)
{
log.Info("C# HTTP trigger function processed a request.");
// Get request body
dynamic data = await req.Content.ReadAsAsync<object>();
// Get values from body data
string webUrl = data?.webUrl;
string callbackUrl = data?.callbackUrl;
var authManager = new OfficeDevPnP.Core.AuthenticationManager();
//Hardcoding ClientId and ClientSecret for demo. For production, store these in a secure location.
var clientContext = authManager.GetAppOnlyAuthenticatedContext(webUrl, "<your_client_id>", "<your_client_secret>");
//Get the PnP Schema
string currentDirectory = executionContext.FunctionDirectory;
DirectoryInfo dInfo = new DirectoryInfo(currentDirectory);
log.Info("Current directory:" + currentDirectory);
var schemaDir = dInfo.Parent.FullName + "\\PnPSiteSchemas";
log.Info("schemaDir:" + schemaDir);
XMLTemplateProvider sitesProvider = new XMLFileSystemTemplateProvider(schemaDir, "");
ProvisioningTemplate template = sitesProvider.GetTemplate("SiteCollectionSchema.xml");
Web web = clientContext.Web;
var author = web.Author;
clientContext.Load(web);
clientContext.Load(author);
clientContext.ExecuteQueryRetry();
//Apply the provisioning template.
log.Info($"Applying Provisioning template to site: {clientContext.Web.Url}");
ProvisioningTemplateApplyingInformation ptai = new ProvisioningTemplateApplyingInformation
{
ProgressDelegate = (message, progress, total) =>
{
log.Info(string.Format("{0:00}/{1:00} - {2}", progress, total, message));
}
};
web.ApplyProvisioningTemplate(template, ptai);
//After provisioning is complete, make a request to the call back Url with data in the body.
log.Info($"Posting to callback url: {callbackUrl}");
await httpClient.PostAsJsonAsync<string>(callbackUrl, $"{{'SiteOwnerEmail' : '{author.Email}'}}");
return req.CreateResponse(HttpStatusCode.OK);
}
}

Now, it is only matter of parsing the data returned from the Azure function and sending the email:



This way, we can perform actions in the Flow after the PnP template has been successfully applied to the site.

Hope this was helpful!

Monday, 22 January 2018

SharePoint Online: Combine and reuse multiple site scripts in a site design

Site scripts and site designs were recently introduced in SharePoint Online/Office 365. They provide a great way to hook into the out of the box site creation dialog to apply customisation to the site being created. Here are some great articles if you want an overview of site scripts and site designs:



In the most basic sense, 

Site scripts are the JSON files used to define the artefacts to be created or the customisation to be applied after the site is created.

Site designs are site templates created by combining one or more site scripts. They can be assigned a title, description, preview image etc. which will be presented to the user in the 'Create new site' dialog.

In this post, let's have a look at how we can combine and re-use multiple site scripts to create site designs.

Here, I have a site script to create a document library on site creation:

{
"$schema": "schema.json",
"actions": [
{
"verb": "createSPList",
"listName": "CnC Documents",
"templateType": 101,
"subactions": [
{
"verb": "SetDescription",
"description": "Custom Document Library created with Site Designs"
}
]
}
],
"bindata": {},
"version": 1
}

Next, I have another site script to trigger a Microsoft Flow on site creation:

{
"$schema": "schema.json",
"actions": [
{
"verb": "triggerFlow",
"url": "https://prod-15.westeurope.logic.azure.com:443/workflows/4333f2255f000000000c6678f11ed42f/triggers/manual/paths/invoke?api-version=2016-06-01&sp=%2Ftriggers%2Fmanual%2Frun&sv=1.0&sig=0000000000000",
"name": "Customise Site with Microsoft PnP",
"parameters": {
"event": "Microsoft Event",
"product": "SharePoint"
}
}
],
"bindata": { },
"version": 1
}

Now, here is a PowerShell script which will create 2 different Site Designs (templates). 

The first site design will only be configured to have the site script to create the document library.

The second site design will contain the same site script to create the document library. But in addition, it will also contain the site script to trigger a Microsoft Flow.

#Using plaintext password for simplicity. Not recommended for production
$secpasswd = ConvertTo-SecureString "the_password" -AsPlainText -Force
$userCredential = New-Object System.Management.Automation.PSCredential ("admin@yourtenant.onmicrosoft.com", $secpasswd)
Connect-SPOService -Url "https://yourtenant-admin.sharepoint.com" -Credential $userCredential
#Get the content of the JSON file and add it to the site script.
$siteScriptListContent = Get-Content 'SiteScripts\site-script-lists.json' -Raw
$siteScriptList = Add-SPOSiteScript -Title "CnC Lists" -Description "Adds a custom document library to the site" -Content $siteScriptListContent
#Get the content of the JSON file and add it to the site script.
$siteScriptTriggerFlowContent = Get-Content 'SiteScripts\site-script-triggerFlow.json' -Raw
$siteScriptTriggerFlow = Add-SPOSiteScript -Title "CnC Trigger Flow" -Description "Triggers a Microsoft Flow" -Content $siteScriptTriggerFlowContent
#Create a basic site design only using the lists site script.
Add-SPOSiteDesign -Title "CnC Basic Communication site" -WebTemplate "68" -SiteScripts $siteScriptList.ID -Description "CnC basic communication site"
#Create an advanced site design by using the lists as well as flow site scripts
Add-SPOSiteDesign -Title "CnC Advanced Communication site" -WebTemplate "68" -SiteScripts $siteScriptList.ID, $siteScriptTriggerFlow.ID -Description "CnC advanced communication site"

Once both site designs are successfully created, they will appear in the "Create a site" dialog on the SharePoint home in Office 365:



This way, you can combine and re-use multiple site scripts to create different site designs as per business requirements.

Troubleshooting:


1) Site scripts and site designs are in preview now and not meant to be used in production. They are also being gradually rolled out to Targeted Release tenants. I was only able to test them in 1 of my tenants. In all other tenants, I was getting the message: "The requested operation is part of an experimental feature that is not supported in the current environment."

2) You will need the latest version of the SharePoint Online Management Shell to get the site design cmdlets: https://www.microsoft.com/en-us/download/details.aspx?id=35588