Firstly, I would like to say that if you want to try SharePoint Webhooks you can find some solution examples in the Microsoft SharePoint github. In my case, I’m working with the approach that uses a SharePoint App to log in against SharePoint.
How to use environmental variables (app settings)
When you create a Function Apps service you must have in mind that at the end, this is like another App Service, so you will have the App settings section available like in a Web App.
To access this section within the Function Apps, go to the “Functions Apps” blade and click on your App. You will see an overview of the app. Click on “Platform features” and click on “Application settings” to open that section like you do with a Web App:
You can think of the application settings like if they were global variables to all your functions, so place in there the static values you want to keep available for them. In my code, I’m going to use the ones I show you in red in the image below:
And now to retrieve these settings within your function, you have to write sentences like “System.Environment.GetEnvironmentVariable(“<name of the app setting>”, EnvironmentVariableTarget.Process)” (example with the above settings):
This way you will retrieve those values in your code.
How to include external libraries in your code (using NuGet packages)
When writing your code, you will probably have the need to include some libraries. There are several ways to do this. I usually use the PnP SharePoint libraries in my developments, so I’m going to show you how to add these to you code.
First way to do this is to use a NuGet package. To use NuGet packages in a C# function, upload a “project.json” file to the function's folder in the function app's file system. You can do this by clicking on your function and clicking in the “View files” button. This will open a new blade in which you can see the files within your function folder. Click on the Upload button (or add a new file) to upload a file like this one:
{
"frameworks": {
"net46":{
"dependencies": {
"SharePointPnPCoreOnline": "2.14.1704"
}
}
} }
Only the .NET Framework 4.6 is supported, so make sure that your “project.json” file specifies the net46 version.
Now that you have included the NuGet, you just have to include the library that you want in your code with the “using” statement: “using OfficeDevPnP.Core;”
But, what if you are going to use the PnP libraries in all your functions? Including this one by one maybe is not the best approach, so I’m going to show you another way to get these libraries in your code.
How to include external libraries in your code (using dll file references)
Because I use the PnP libraries on almost all my functions, I like to have a common place in which to place the dlls, in order to be able to use the libraries from all my functions. So, the idea is to upload the dll file somewhere into the “wwwroot” folder of the App.
To do this, I like to use the Kudu tool to explore the folders structure of the App Service. Go again to the “Platform feature” of the Functions App and click on “Advanced tools (Kudu)” to open the tool in another tab.
Click on “Debug Console” (CMD or PowerShell) and navigate to the “wwwroot” folder. Notice that you will see one folder per each function you have created in your “Functions App”. Create here a new folder (choose the name you want) and upload the dll in it. This is an example of how it looks like in my App:
Now that you have uploaded the library that you want to use within your code, you just have to write #r “<relative path to the wwwroot folder where the dll is>” and consume it with the “using” statement”. This is an example of how it would look like in the code:
The #r is the way you reference libraries included in “Functions App” by default, so this is not really a lot different than that. Now you are able to use everything included in that library.
This is everything I wanted to show you in this post. To avoid making this post longer and difficult to read, I leave a PS with a full example of code (as I promised in my last post) in case that you want to give it a try along with SharePoint Webhooks.
I hope this have been interesting for you and that it can help someone reading it.
--------------------------------------------------------------------------------------------------------------------------
PS (To use this example, use the PnP NuGet package. Webhooks requests must be resolved within 5 seconds. This is just an example of simple SP operations in order to show you how it works and to ensure the code finishes in time. In a “real world” scenario it is recommended to work in a total asynchronous mode. This code creates a list with name “WebHookHistory” and inserts an item with a message of the operation made in the list for which you have a webhook subscription):
#r "Newtonsoft.Json"
#r "Microsoft.WindowsAzure.Storage"
using System;
using System.Net;
using Newtonsoft.Json;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Queue;
using OfficeDevPnP.Core;
using Microsoft.SharePoint.Client;
public static async Task<object> Run(HttpRequestMessage req, TraceWriter log)
{
log.Info($"Webhook was triggered!");
string spTenantNameKey = "SP_Tenant_Name";
string spAppClientIdKey = "SPApp_ClientId";
string spAppClientSecretKey = "SPApp_ClientSecret";
string spTenantNameValue = System.Environment.GetEnvironmentVariable(spTenantNameKey, EnvironmentVariableTarget.Process);
string spAppClientIdValue = System.Environment.GetEnvironmentVariable(spAppClientIdKey, EnvironmentVariableTarget.Process);
string spAppClientSecretValue = System.Environment.GetEnvironmentVariable(spAppClientSecretKey, EnvironmentVariableTarget.Process);
// Grab the validationToken URL parameter
string validationToken = req.GetQueryNameValuePairs()
.FirstOrDefault(q => string.Compare(q.Key, "validationtoken", true) == 0)
.Value;
// If a validation token is present, we need to respond within 5 seconds by
// returning the given validation token. This only happens when a new
// web hook is being added
if (validationToken != null)
{
log.Info($"Validation token {validationToken} received");
var response = req.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(validationToken);
return response;
}
log.Info($"SharePoint triggered our webhook...great :-)");
var content = await req.Content.ReadAsStringAsync();
log.Info($"Received following payload: {content}");
var notifications = JsonConvert.DeserializeObject<ResponseModel<NotificationModel>>(content).Value;
log.Info($"Found {notifications.Count} notifications");
if (notifications.Count > 0)
{
log.Info($"Processing notifications...");
foreach(var notification in notifications)
{
ClientContext cc = null;
try
{
#region Setup an app-only client context
OfficeDevPnP.Core.AuthenticationManager am = new OfficeDevPnP.Core.AuthenticationManager();
string url = String.Format("https://{0}{1}", spTenantNameValue, notification.SiteUrl);
string clientId = spAppClientIdValue;
string clientSecret = spAppClientSecretValue;
cc = am.GetAppOnlyAuthenticatedContext(url, clientId, clientSecret);
#endregion
#region Grab the list for which the web hook was triggered
ListCollection lists = cc.Web.Lists;
Guid listId = new Guid(notification.Resource);
IEnumerable<List> results = cc.LoadQuery<List>(lists.Where(lst => lst.Id == listId));
cc.ExecuteQueryRetry();
List changeList = results.FirstOrDefault();
if (changeList == null)
{
// list has been deleted inbetween the event being fired and the event being processed
return new HttpResponseMessage(HttpStatusCode.OK);
}
#endregion
#region Grab the list used to write the web hook history
// Ensure reference to the history list, create when not available
List historyList = cc.Web.GetListByTitle("WebHookHistory");
if (historyList == null)
{
historyList = cc.Web.CreateList(ListTemplateType.GenericList, "WebHookHistory", false);
}
#endregion
#region Grab the list changes and do something with them
// grab the changes to the provided list using the GetChanges method
// on the list. Only request Item changes as that's what's supported via
// the list web hooks
ChangeQuery changeQuery = new ChangeQuery(false, true);
changeQuery.Item = true;
changeQuery.FetchLimit = 1000; // Max value is 2000, default = 1000
// Start pulling down the changes
bool allChangesRead = false;
do
{
ChangeToken lastChangeToken = new ChangeToken();
lastChangeToken.StringValue = string.Format("1;3;{0};{1};-1", notification.Resource, DateTime.Now.AddMinutes(-5).ToUniversalTime().Ticks.ToString());
// Assign the change token to the query...this determines from what point in
// time we'll receive changes
changeQuery.ChangeTokenStart = lastChangeToken;
// Execute the change query
var changes = changeList.GetChanges(changeQuery);
cc.Load(changes);
cc.ExecuteQueryRetry();
if (changes.Count > 0)
{
foreach (Change change in changes)
{
lastChangeToken = change.ChangeToken;
if (change is ChangeItem)
{
// do "work" with the found change
ListItemCreationInformation newItem = new ListItemCreationInformation();
ListItem item = historyList.AddItem(newItem);
item["Title"] = string.Format("List {0} had a Change of type \\\\"{1}\\\\" on the item with Id {2}.", changeList.Title, change.ChangeType.ToString(), (change as ChangeItem).ItemId);
item.Update();
cc.ExecuteQueryRetry();
}
}
// We potentially can have a lot of changes so be prepared to repeat the
// change query in batches of 'FetchLimit' untill we've received all changes
if (changes.Count < changeQuery.FetchLimit)
{
allChangesRead = true;
}
}
else
{
allChangesRead = true;
}
// Are we done?
} while (allChangesRead == false);
#endregion
}
catch (Exception ex)
{
// Log error
Console.WriteLine(ex.ToString());
}
finally
{
if (cc != null)
{
cc.Dispose();
}
}
}
}
// if we get here we assume the request was well received
return new HttpResponseMessage(HttpStatusCode.OK);
}
// supporting classes
public class ResponseModel<T>
{
[JsonProperty(PropertyName = "value")]
public List<T> Value { get; set; }
}
public class NotificationModel
{
[JsonProperty(PropertyName = "subscriptionId")]
public string SubscriptionId { get; set; }
[JsonProperty(PropertyName = "clientState")]
public string ClientState { get; set; }
[JsonProperty(PropertyName = "expirationDateTime")]
public DateTime ExpirationDateTime { get; set; }
[JsonProperty(PropertyName = "resource")]
public string Resource { get; set; }
[JsonProperty(PropertyName = "tenantId")]
public string TenantId { get; set; }
[JsonProperty(PropertyName = "siteUrl")]
public string SiteUrl { get; set; }
[JsonProperty(PropertyName = "webId")]
public string WebId { get; set; }
}