We are living in a world which everyday you wake up with some brilliant features added to your favorite libraries and you can’t wait to use them somewhere in your solution.

In this post I will show you how easy is to call Azure Functions from SharePoint Framework Web Part and also how you can use React Hooks to simplifies your components and get rid of extra classes and life cycles around them.

As you know React Hooks are new addition to React 16.8 and with the release of SharePoint Framework 1.9.1 you can have them by default which is fantastic! Azure Functions also are so popular as they are so useful in many cases, they are cheap and can be implemented really quick even from Azure Portal and without having any editors! also they are really flexible, you can have a function runs on a timer, or can be triggered when a HTTP endpoint is hit, and last but not least they can be secured with AAD.

So let’s finish the introduction and jump to the implementation, we will create a web part that calls two Azure Functions, one to get the provisioning template and one to apply a provisioning template.

You can find the source code here.

Azure Functions Code

Let’s start from Azure Functions, I use Visual Studio as it has default templates for Azure Functions and gives me ability to publish or debug the solution easily.

Open Visual Studio, open New Project window and select Azure Functions, give it a name and click OK.

In next window select Azure Functions v1 (.Net Framework) and Http trigger, then click OK.

Right click on dependencies, select Manage NuGet packages, in the browse tab search for SharePointPnPCoreOnline and install it (we need this package to get Azure Active Directory App-Only context):

Remove function1.cs and create below folder structure for your solution:

Certificate

This will be used to authenticate your Application against Azure AD, while requesting the App Only access token. 

To create a self signed certificate, download Create-SelfSignedCertificate.ps1, open PowerShell and run below command:

.\Create-SelfSignedCertificate.ps1 -CommonName "NAME" -StartDate 2019-08-11 -EndDate 2025-08-11 -Password (ConvertTo-SecureString -String "PASSWORD" -AsPlainText -Force)
  • NAME: your preferred name for the certificate (e.x. ProvisioningApp).
  • PASSWORD: replace it with your own password and keep it somewhere safe as we need in next steps.
  • Change the StartDate and EndDate (you can use Get-Date in PowerShell).

This script gives you two files:

  • .cer: to be uploaded to your App Registration.
  • .pfx: copy this file to Cert folder of your solution.

Constants

Constants are immutable values which are known at compile time and do not change for the life of the program.

Under Constants folder create a class called configs and update it with:

namespace ProvisioningApp.Constants
{
  public static class Configs
  {
    public const string exportedTemplateName = "PnPProvisioningTemplate.xml";
    public const string clientIdKey = "CLIENTID";
    public const string certificatePathKey = "CERTIFIATE";
    public const string passwordKey = "PASSWORD";
    public const string tenantKey = "TENANT";
  }
}

There are the application settings keys we will setup later on.

Helper

Under Utils folder, create a class called Helper.cs and update it with:

using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core;
using System;
using ProvisioningApp.Constants;

namespace ProvisioningApp.Utils
{
  public static class Helper
  {
    public static ClientContext GetADAppOnlyContext(string siteUrl, string appDirectory)
    {
      var authMgr = new AuthenticationManager();

      string certificateName = Environment.GetEnvironmentVariable(Configs.certificatePathKey);
      string password = Environment.GetEnvironmentVariable(Configs.passwordKey);
      string clientId = Environment.GetEnvironmentVariable(Configs.clientIdKey);
      string tenant = Environment.GetEnvironmentVariable(Configs.tenantKey);
      string certificatePath = $"{appDirectory}\\Cert\\{certificateName}";

      if (string.IsNullOrEmpty(clientId))
        throw new ArgumentException($"Missing required environment variable '{Configs.clientIdKey}'");

      if (string.IsNullOrEmpty(certificateName))
        throw new ArgumentException($"Missing required environment variable '{Configs.certificatePathKey}'");

      if (string.IsNullOrEmpty(password))
        throw new ArgumentException($"Missing required environment variable '{Configs.passwordKey}'");

      if (string.IsNullOrEmpty(tenant))
        throw new ArgumentException($"Missing required environment variable '{Configs.tenantKey}'");

      return authMgr.GetAzureADAppOnlyAuthenticatedContext(siteUrl, clientId, tenant, certificatePath, password);
    }
  }
}

As you can see this class has only one method that returns Client Context we need to perform our operations against SharePoint.

Models

Under Models folder we have two classes:

LoadProvisioningInfo.cs: defines the properties in the request body we need to get provisioning template:

using System;
using System.Collections.Generic;
using System.Text;
using OfficeDevPnP.Core.Framework.Provisioning.Model;
namespace ProvisioningApp.Models
{
  public class LoadProvisioningInfo
  {
    public string WebUrl { get; set; }

    public Handlers Handlers { get; set; }
      
  }
}

ApplyProvisioningInfo.cs: data we need to apply the provisioning template.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ProvisioningApp.Models
{
  public class ApplyProvisioningInfo
  {
    public string WebUrl { get; set; }

    public string Template { get; set; }
  }
}

Functions

Now we are ready to create out functions, under Functions folder create two Azure Functions:

  • GetProvisioningTemplate.cs
  • ApplyProvisioningTemplate.cs

Open GetProvisioningTemplate.cs and paste below code:

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Xml.Linq;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core.Framework.Provisioning.Connectors;
using OfficeDevPnP.Core.Framework.Provisioning.Model;
using OfficeDevPnP.Core.Framework.Provisioning.ObjectHandlers;
using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml;
using ProvisioningApp.Models;
using ProvisioningApp.Constants;
using ProvisioningApp.Utils;
using System.IO;

namespace ProvisioningApp
{
  public static class GetProvisioningTemplate
  {
    [FunctionName("GetProvisioningTemplate")]
    public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log, ExecutionContext context)
    {
      log.Info("C# HTTP trigger function processed a request.");
      
      try
      {
        var requestBody = await req.Content.ReadAsAsync<LoadProvisioningInfo>();

        if (requestBody == null)
          return req.CreateResponse(System.Net.HttpStatusCode.BadRequest);
        
        GenerateProvisioningTemplate(requestBody,context.FunctionAppDirectory);
        log.Info("Template has been created");
        var xDocument = XDocument.Load($"{Path.GetTempPath()}\\{Configs.FileName}");
        // convert the xml into string
        string xml = xDocument.ToString();
        var result = new HttpResponseMessage(System.Net.HttpStatusCode.OK);
        result.Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes(xml));
        result.Content.Headers.ContentType = new MediaTypeHeaderValue("text/xml");
        
        return result;

      }
      catch (Exception ex)
      {
        return req.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError,ex.Message);
      }

    }

    private static void GenerateProvisioningTemplate(LoadProvisioningInfo info, string appDirectory)
    {
      var context =  Helper.GetADAppOnlyContext(info.WebUrl, appDirectory);

      using (context)
      {
        Web web = context.Web;
        context.Load(web, w => w.Title);
        context.ExecuteQueryRetry();
        ProvisioningTemplateCreationInformation ptci
                   = new ProvisioningTemplateCreationInformation(context.Web);
        
        // Create FileSystemConnector to store a temporary copy of the template 
        ptci.FileConnector = new FileSystemConnector(Path.GetTempPath(), "");
        ptci.PersistBrandingFiles = true;

        ptci.HandlersToProcess = info.Handlers;
        // Execute actual extraction of the template
        ProvisioningTemplate template = context.Web.GetProvisioningTemplate(ptci);

        // We can serialize this template to save and reuse it
        // Optional step 
        XMLTemplateProvider provider =
                new XMLFileSystemTemplateProvider(Path.GetTempPath(), "");
        provider.SaveAs(template, Configs.FileName);        
      }
    }

    

  }
}

Couple of notes to mention:

  • ReadAsAsync deserialize  the request body to the class we defined (LoadProvisioningInfo).
  • Path.GetTempPath gives the temporary path in Azure to save your folder (in our case we are going to save the template file as XML).
  • ProvisioningTemplate class provides everything you need to Get or Apply a provisioning template.

Open ApplyProvisioningTemplate.cs and paste this code:

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.Azure.WebJobs.Host;
using Microsoft.SharePoint.Client;
using OfficeDevPnP.Core.Framework.Provisioning.Model;
using OfficeDevPnP.Core.Framework.Provisioning.Providers.Xml;
using ProvisioningApp.Models;
using ProvisioningApp.Utils;

namespace ProvisioningApp.Functions
{
  public static class ApplyProvisioningTemplate
  {
    [FunctionName("ApplyProvisioningTemplate")]
    public static async Task<HttpResponseMessage> Run([HttpTrigger(AuthorizationLevel.Function, "post", Route = null)]HttpRequestMessage req, TraceWriter log, ExecutionContext context)
    {
      log.Info("C# HTTP trigger function processed a request.");

      try
      {
        var requestBody = await req.Content.ReadAsAsync<ApplyProvisioningInfo>();

        if (requestBody == null)
          return req.CreateResponse(HttpStatusCode.BadRequest);

        var ctx = Helper.GetADAppOnlyContext(requestBody.WebUrl, context.FunctionAppDirectory);
        using (ctx)
        {
          Web web = ctx.Web;
          ctx.Load(web, w => w.Title);
          ctx.ExecuteQueryRetry();

          // Configure the XML file system provider
          XMLTemplateProvider provider =
          new XMLFileSystemTemplateProvider(Path.GetTempPath(), "");

          byte[] byteArray = Encoding.ASCII.GetBytes(requestBody.Template);
          MemoryStream stream = new MemoryStream(byteArray);

          // Load the template from the XML stored copy
          ProvisioningTemplate template = provider.GetTemplate(stream);

          // We can also use Apply-PnPProvisioningTemplate
          web.ApplyProvisioningTemplate(template);
        }

        return req.CreateErrorResponse(HttpStatusCode.OK, "Done!");

      }
      catch (Exception ex)
      {
        return req.CreateErrorResponse(System.Net.HttpStatusCode.InternalServerError, ex.Message);
      }

    }
  }
}

Publish functions

  • If you haven’t already signed-in to your Azure account from Visual Studio, select Sign-in.
  • In Solution Explorer, right-click the project and select Publish.
  • In the Pick a publish target dialog, use the publish options as specified in the table below the image:
  • Select Publish.
  • In the App Service: Create new dialog, enter the hosting settings.
  • Select Create to create a function app and related resources in Azure with these settings and deploy your function project code.

Securing the Azure function app

  • Open Azure Portal https://portal.azure.com.
  • Click App Services and find the app you just created.
  • Click “Platform features” tab.
  • Under Networking, click “Authentication / Authorization”.
  • In the option “App Service Authentication ” , select “ON”.
  • For “Action to take when request is not authenticated” option, select “Log in with Azure Active Directory”.
  • Under “Authentication Providers”, select “Azure Active Directory”.
  • Select “Management mode” as Express.
  • Create new AD App or select an existing one.
  • Click OK and then Save.

Setting up the Azure AD app for app-only access

Now we need to configure our app registration we just created in previous step.

  • Select Azure Active Director, App Registration and then the App you created.
  • Click on “Certificates & secrets”.
  • Click on the “Upload certificate” button.
  • Select the .CER file you generated earlier and click on “Add” to upload it.
  • Click on API Permissions.
  • Click on the “Add a permission” button.
  • Choose the following permissions:

Update App Settings

We are almost there! from Azure portal open the app service you created,

  • Click “Platform features” tab.
  • click on “Configuration”.
  • Create new key/value entries under ‘App settings’ as per the following table:
KeyValue
CERTIFIATEprovisioningapp.pfx
PASSWORD Password you set for the certificate file
CLIENTID Application Registration Client ID
TENANTYour tenant (e.x. contoso.onmicrosoft.com)

Enable CORS on Azure Function

  • Click Platform features.
  • Under API, click CORS.
  • Specify the Office 365 tenant domain url and SharePoint local workbench url.
  • Click Save.

Test the functions

It’s time to test our functions, there are different ways of testing your functions such as Azure Portal, Postman, Fiddler and other tools as well Visual Studio which gives you ability to debug the code. We are going to test it from Azure Portal:

  • Open the App Service and select GetProvisioningTemplate function.
  • Click on Run button:
  • Add headers and body as below and click Run, if everything is OK you should get response like this:

Summary

We’re now complete with setting up the Azure Functions and they are ready to be called from our SharePoint Framework Web Part, in next part we will discuss the code to call these functions from our web part as well as React Hooks.

How useful was this post?

Click on a star to rate it!

Average rating / 5. Vote count: