Friday, 12 May 2017

Calling an Azure Function from a SharePoint Framework web part (SPFx) – part 2: calling from SPFx and passing data

In the previous post we looked at the work to get started with an Azure Function which can be called from an SPFx web part, and can use the PnP core library. Having got our Function set up with CORS, NuGet support for PnP, and deployment from source control we’re now ready to finalise the code. As a recap, I’m splitting the steps and code into these two articles:

Part 1 – setup, use of PnP Core, CORS and continuous deployment (previous article):
  • Initial creation of Function
  • Deployment of function code (using Continuous Deployment from GitHub)
  • Allowing CORS on an Azure Function so that it can be called from JavaScript (including a special entry for the SPFx workbench)
  • Use of SharePoint PnP Core library - via Azure Functions NuGet support
  • Use of App Settings (environment variables) in an Azure Function
Part 2 – calling from SPFx and passing data (this article):
  • Code for the Azure Function side (i.e. the work to create a modern page from some passed parameters)
  • Code for the SPFx side
  • Passing data to the function and adding correct headers
  • Returning data from the function, and displaying success/failure in our SPFx web part

The code – Azure Function side

I’ll talk about a couple of notable things first, and then show the full code.

Collecting data passed from client

We’re going to assume the caller is going to POST data to the function as JSON in the request body – the boilerplate function code does this too, so we’re just extending things a little. We can then access the pieces of data through the Content.ReadAsync() method using a C# dynamic variable and plain object type:

// collect site/page details from request body.. 
  dynamic data = await req.Content.ReadAsAsync<object>(); 
  string siteUrl = data.SiteUrl; 
  string pageName = data.PageName; 
  string pageText = data.PageText; 

  log.Info($"Received siteUrl={siteUrl}, pageName={pageName}, pageText={pageText}"); 

If you wanted to be “tighter” on how data is passed to the server, you could implement a specific class with appropriate fields/properties of course, and specify that as the type that .NET’s ReadAsAsync method should attempt to deserialize the request body to (instead of ‘object’). But, the code above works as a simple way to collect some data passed to the Function – on the client side, of course, we would ensure an appropriate string of JSON with the same properties is set to the body of the request (shown below).

Returning in the right way to the client

We want our endpoint to respect the HTTP conventions here by returning a HTTP 200 if all is good, some appropriate 4xx error code if the request isn’t valid, and so on. We can use the HttpRequestMessage.CreateResponse() method to do this. One example where we might want our Function to send back an error is when it’s called from the SharePoint Framework local workbench instead of a real site – if we don’t have a site, how can our Function know where to create the page? So, I send back a HTTP 400 (bad request) in this case:

if (siteUrl.Contains("")) { 
   // N.B. the “” URL indicates the local workbench in SPFx.. 
  return req.CreateResponse(HttpStatusCode.BadRequest, "Error: please run in the context of a real SharePoint site, not the local workbench. We need this to know which site to create the page in!"); 

..and now this will show up in browser dev tools/Fiddler/other monitoring tools in the right way and so on.

The full code for the Azure Function to create a modern page is:

We’ll get to the SPFx web part soon, but note that the Azure portal gives you a nice way to test your Function before you’ve actually created a caller. You can craft the request body so that it contains the JSON with the properties the function expects, and then press the “Run” button to see the results. Your Function will execute, and any logging messages you have will be output to the console:


Integrating the function with an SPFx web part

So, let’s create a simple SPFx web part which calls our Function. We’ll provide a simple form to collect parameters for the page name and default content, and a button to actually call the Function and get the page created. I didn’t do anything fancy here – I ended up with this:


So let’s head towards that. First, we need the Function’s URL – we can get this from the Azure portal. Find this link to do that:


Now we need to think about calling our Function from SPFx. You can do this using the HttpClient object of course, but there are some things to know to structure the request correctly in terms of the headers and body content. We also need to collect some information from our form fields, but the important thing when POSTing data in SPFx is the use of the Headers and IHttpClientOptions objects in the SharePoint Framework:

const requestHeaders: Headers = new Headers();
requestHeaders.append("Content-type", "application/json");
requestHeaders.append("Cache-Control", "no-cache");

let siteUrl: string = this.context.pageContext.web.absoluteUrl;
let pageName: string = (document.getElementById("txtPageName")).value;
let pageText: string = (document.getElementById("txtPageText")).value;

console.log(`SiteUrl: '${siteUrl}', PageName: '${pageName}.aspx', PageText: '${pageText}'`);

const postOptions: IHttpClientOptions = {
   headers: requestHeaders,
   body: `{ SiteUrl: '${siteUrl}', PageName: '${pageName}.aspx', PageText: '${pageText}' }`

..and then we can use this set of headers/body content (in the ‘postOptions’ object) in a POST request to our Function with the method:, HttpClient.configurations.v1, postOptions).then((response: HttpClientResponse) => {
    // code omitted..

The important thing here is to trap any non-success responses from the server, by ensuring your call to has both an 'onFulfilled' and 'onRejected' handler (the first and second parameter respectively). You can see this in the full code for the SPFx web part below:

So, with the code for both the server and client side sorted, our web part can now create a modern page using the PnP methods. Because the Azure Function sends back the appropriate responses, it’s easy for us to display success/failure in the UI.





Assuming all was well, a new modern page is created in the current site as you’d expect:


Note that if you want to recreate this web part, you'd also need to some styles in your *.module.scss file:


In addition to “timer” processes and processing of Azure BLOBs and Queue items, Azure Functions are a great way to add little bits of .NET code to your client-side apps and web parts. We showed it in the context of a SharePoint Framework web part here, but any kind of JavaScript could call our Function – that includes mobile apps, SharePoint Content Editor web parts, SPFx extensions/customizers (the replacement for JSLink/CustomActions etc.) and so on. We went through enabling CORS, bringing in the PnP Core component through the project.json file for NuGet, setting up source control integration and other steps.

Other things you might need to think of in production include authentication. My example used “Function auth + SharePointOnlineCredentials”, but you might want to secure your Function with AAD instead and use true app authentication within your code. That would mean first registering an AAD app and obtaining an access token through adal.js, which is certainly more complex and presents an issue around Reply URLs if your web part can be on multiple pages. Vesa and Waldek talked about an interesting alternative which calls the Function through an IFrame (with cookies), but that only works if your code can use app-only auth. It would be nice to have better options than this in the future to be honest.

Consider also that Functions are evolving beyond .csx files, so although you could always reference your own DLLs from there and factor your code that way, other more direct ways of using class libraries now exist too – see Publishing a .NET class library as a Function App

Either way, Functions are a great tool and as a developer you’ll benefit from being familiar with them :)

No comments: