/// <summary> /// Processes a received notification. This typically is triggered via an Azure Web Job that reads the Azure storage queue /// </summary> /// <param name="notification">Notification to process</param> public void ProcessNotification(NotificationModel notification) { ClientContext cc = null; try { #region Setup an app-only client context using Azure AD certificate based authentication string url = String.Format("https://{0}{1}", CloudConfigurationManager.GetSetting("TenantName"), notification.SiteUrl); cc = ContextProvider.GetAppOnlyClientContext(url); #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; } #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 // grab last change token from database if possible using (SharePointWebHooks dbContext = new SharePointWebHooks()) { ChangeToken lastChangeToken = null; Guid id = new Guid(notification.SubscriptionId); var listWebHookRow = dbContext.ListWebHooks.Find(id); if (listWebHookRow != null) { lastChangeToken = new ChangeToken(); lastChangeToken.StringValue = listWebHookRow.LastChangeToken; } // Start pulling down the changes bool allChangesRead = false; do { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net if (lastChangeToken == null) { lastChangeToken = new ChangeToken(); // See https://blogs.technet.microsoft.com/stefan_gossner/2009/12/04/content-deployment-the-complete-guide-part-7-change-token-basics/ 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 DoWork(cc, changeList, historyList, change); } } // 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); // Persist the last used changetoken as we'll start from that one // when the next event hits our service if (listWebHookRow != null) { // Only persist when there's a change in the change token if (!listWebHookRow.LastChangeToken.Equals(lastChangeToken.StringValue, StringComparison.InvariantCultureIgnoreCase)) { listWebHookRow.LastChangeToken = lastChangeToken.StringValue; dbContext.SaveChanges(); } } else { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net dbContext.ListWebHooks.Add(new ListWebHooks() { Id = id, ListId = listId, LastChangeToken = lastChangeToken.StringValue, }); dbContext.SaveChanges(); } } #endregion #region "Update" the web hook expiration date when needed // Optionally add logic to "update" the expirationdatetime of the web hook // If the web hook is about to expire within the coming 5 days then prolong it if (notification.ExpirationDateTime.AddDays(-5) < DateTime.Now) { WebHookManager webHookManager = new WebHookManager(); Task <bool> updateResult = Task.WhenAny( webHookManager.UpdateListWebHookAsync( url, listId.ToString(), notification.SubscriptionId, CloudConfigurationManager.GetSetting("WebHookEndPoint"), DateTime.Now.AddMonths(3), this.accessToken) ).Result; if (updateResult.Result == false) { throw new Exception(String.Format("The expiration date of web hook {0} with endpoint {1} could not be updated", notification.SubscriptionId, CloudConfigurationManager.GetSetting("WebHookEndPoint"))); } } #endregion } catch (Exception ex) { // Log error Console.WriteLine(ex.ToString()); } finally { if (cc != null) { cc.Dispose(); } } }
/// <summary> /// Processes a received notification. This typically is triggered via an Azure Web Job that reads the Azure storage queue /// </summary> /// <param name="notification">Notification to process</param> public void ProcessNotification(NotificationModel notification) { ClientContext cc = null; try { #region Setup an app-only client context var am = new AuthenticationManager(); var url = $"https://{CloudConfigurationManager.GetSetting("TenantName")}{notification.SiteUrl}"; var realm = TokenHelper.GetRealmFromTargetUrl(new Uri(url)); var clientId = CloudConfigurationManager.GetSetting("ClientId"); var clientSecret = CloudConfigurationManager.GetSetting("ClientSecret"); cc = new Uri(url).DnsSafeHost.Contains("spoppe.com") ? am.GetAppOnlyAuthenticatedContext(url, realm, clientId, clientSecret, acsHostUrl: "windows-ppe.net", globalEndPointPrefix: "login") : am.GetAppOnlyAuthenticatedContext(url, clientId, clientSecret); cc.ExecutingWebRequest += Cc_ExecutingWebRequest; #endregion #region Grab the list for which the web hook was triggered var lists = cc.Web.Lists; var listId = new Guid(notification.Resource); var results = cc.LoadQuery(lists.Where(lst => lst.Id == listId)); cc.ExecuteQueryRetry(); var changeList = results.FirstOrDefault(); if (changeList == null) { // list has been deleted in between the event being fired and the event being processed return; } #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 var changeQuery = new ChangeQuery(false, true) { Item = true, FetchLimit = 1000, // Max value is 2000, default = 1000 DeleteObject = false, Add = true, Update = true, SystemUpdate = false }; // grab last change token from database if possible using (var dbContext = new SharePointWebHooks()) { ChangeToken lastChangeToken = null; var id = new Guid(notification.SubscriptionId); var listWebHookRow = dbContext.ListWebHooks.Find(id); if (listWebHookRow != null) { lastChangeToken = new ChangeToken { StringValue = listWebHookRow.LastChangeToken }; } // Start pulling down the changes var allChangesRead = false; do { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net if (lastChangeToken == null) { lastChangeToken = new ChangeToken { StringValue = $"1;3;{notification.Resource};{DateTime.Now.AddMinutes(-5).ToUniversalTime().Ticks.ToString()};-1" }; // See https://blogs.technet.microsoft.com/stefan_gossner/2009/12/04/content-deployment-the-complete-guide-part-7-change-token-basics/ } // 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 item is changed more than once var uniqueChanges = changes.Cast <ChangeItem>().AsEnumerable().DistinctBy(change => change.ItemId).ToList(); if (uniqueChanges.Any()) { foreach (var change in uniqueChanges) { lastChangeToken = change.ChangeToken; try { // do "work" with the found change DoWork(cc, changeList, change); } catch (Exception) { // ignored } } // We potentially can have a lot of changes so be prepared to repeat the // change query in batches of 'FetchLimit' until we've received all changes if (changes.Count < changeQuery.FetchLimit) { allChangesRead = true; } } else { allChangesRead = true; } // Are we done? } while (allChangesRead == false); // Persist the last used change token as we'll start from that one // when the next event hits our service if (listWebHookRow != null) { // Only persist when there's a change in the change token if (!listWebHookRow.LastChangeToken.Equals(lastChangeToken.StringValue, StringComparison.InvariantCultureIgnoreCase)) { listWebHookRow.LastChangeToken = lastChangeToken.StringValue; dbContext.SaveChanges(); } } else { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net dbContext.ListWebHooks.Add(new ListWebHooks() { Id = id, ListId = listId, LastChangeToken = lastChangeToken.StringValue, }); dbContext.SaveChanges(); } } #endregion #region "Update" the web hook expiration date when needed // Optionally add logic to "update" the expiration date time of the web hook // If the web hook is about to expire within the coming 5 days then prolong it if (notification.ExpirationDateTime.AddDays(-5) >= DateTime.Now) { return; } var webHookManager = new WebHookManager(); var updateResult = Task.WhenAny( webHookManager.UpdateListWebHookAsync( url, listId.ToString(), notification.SubscriptionId, CloudConfigurationManager.GetSetting("WebHookEndPoint"), DateTime.Now.AddMonths(3), _accessToken) ).Result; if (updateResult.Result == false) { throw new Exception( $"The expiration date of web hook {notification.SubscriptionId} with endpoint {CloudConfigurationManager.GetSetting("WebHookEndPoint")} could not be updated"); } #endregion } catch (Exception ex) { // Log error Console.WriteLine(ex.ToString()); } finally { // ReSharper disable once ConstantConditionalAccessQualifier cc?.Dispose(); } }
/// <summary> /// Processes a received notification. This typically is triggered via an Azure Web Job that reads the Azure storage queue /// </summary> /// <param name="notification">Notification to process</param> public async Task ProcessNotification(NotificationModel notification, TraceWriter log) { ClientContext cc = null; ClientContext acc = null; string notificationStep = "1"; try { #region Setup an app-only client context AuthenticationManager am = new AuthenticationManager(); notificationStep = "1a"; string url = String.Format("https://{0}{1}", System.Environment.GetEnvironmentVariable("TenantName"), notification.SiteUrl); notificationStep = "1b" + url; string realm = TokenHelper.GetRealmFromTargetUrl(new Uri(url)); string approvalUrl = System.Environment.GetEnvironmentVariable("ApprovalSiteUrl"); log.Info("ApprovalSiteUrl: " + approvalUrl); notificationStep = "1b" + url; string approvalRealm = TokenHelper.GetRealmFromTargetUrl(new Uri(approvalUrl)); notificationStep = "1c"; string clientId = System.Environment.GetEnvironmentVariable("ClientId"); notificationStep = "1d"; string clientSecret = System.Environment.GetEnvironmentVariable("ClientSecret"); notificationStep = "2"; if (new Uri(url).DnsSafeHost.Contains("spoppe.com")) { log.Info("Government cloud login (I think)"); cc = am.GetAppOnlyAuthenticatedContext(url, realm, clientId, clientSecret, acsHostUrl: "windows-ppe.net", globalEndPointPrefix: "login"); acc = am.GetAppOnlyAuthenticatedContext(approvalUrl, approvalRealm, clientId, clientSecret, acsHostUrl: "windows-ppe.net", globalEndPointPrefix: "login"); log.Info("Government cloud login (I think) token received"); } else { log.Info("App only login"); notificationStep = "2"; cc = am.GetAppOnlyAuthenticatedContext(url, clientId, clientSecret); acc = am.GetAppOnlyAuthenticatedContext(approvalUrl, clientId, clientSecret); log.Info("App only login token received"); } notificationStep = "3"; cc.ExecutingWebRequest += Cc_ExecutingWebRequest; #endregion notificationStep = "3a"; #region Grab the list for which the web hook was triggered ListCollection lists = cc.Web.Lists; notificationStep = "3b"; Guid listId = new Guid(notification.Resource); notificationStep = "3c"; log.Info("Loaded source list"); IEnumerable <List> results = cc.LoadQuery <List>(lists.Where(lst => lst.Id == listId)); notificationStep = $"3d-{listId.ToString()},{notification.Resource.ToString()}, {notification.SiteUrl}"; cc.ExecuteQueryRetry(); log.Info("Loaded source list loaded"); notificationStep = "3e"; List notificationSourceList = results.FirstOrDefault(); if (notificationSourceList == null) { // list has been deleted inbetween the event being fired and the event being processed return; } notificationStep = "4"; #endregion #region Grab the list used to write the web hook history // Ensure reference to the history list, create when not available List approvalsList = acc.Web.GetListByTitle("Approvals"); if (approvalsList == null) { log.Info("Creating approvals list as not in place"); approvalsList = acc.Web.CreateList(ListTemplateType.GenericList, "Approvals", false); this.AddTextField(approvalsList, "ClientState", "ClientState", cc); this.AddTextField(approvalsList, "SubscriptionId", "SubscriptionId", cc); this.AddTextField(approvalsList, "ExpirationDateTime", "ExpirationDateTime", cc); this.AddTextField(approvalsList, "Resource", "Resource", cc); this.AddTextField(approvalsList, "TenantId", "TenantId", cc); this.AddTextField(approvalsList, "SiteUrl", "SiteUrl", cc); this.AddTextField(approvalsList, "WebId", "WebId", cc); this.AddTextField(approvalsList, "ItemId", "ItemId", cc); this.AddTextField(approvalsList, "ActivityId", "Activity Id", cc); this.AddTextField(approvalsList, "EditorEmail", "EditorEmail", cc); this.AddTextField(approvalsList, "Activity", "Activity", cc); approvalsList.Update(); acc.ExecuteQuery(); } notificationStep = "5"; #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.RecursiveAll = true; changeQuery.User = true; changeQuery.FetchLimit = 1000; // Max value is 2000, default = 1000 notificationStep = "6"; ChangeToken lastChangeToken = null; Guid id = new Guid(notification.SubscriptionId); string storageConnectionString = System.Environment.GetEnvironmentVariable("StorageConnectionString"); const string tableName = "crosssiteappchangetokens"; // Connect to storage account / container var storageAccount = Microsoft.WindowsAzure.Storage.CloudStorageAccount.Parse(storageConnectionString); CloudTableClient tableClient = storageAccount.CreateCloudTableClient(); CloudTable table = tableClient.GetTableReference(tableName); notificationStep = "7"; await table.CreateIfNotExistsAsync(); TableResult result = await table.ExecuteAsync(TableOperation.Retrieve <TableChangeToken>("List", id.ToString())); TableChangeToken loadedChangeToken = null; if (result.Result != null) { lastChangeToken = new ChangeToken(); loadedChangeToken = (result.Result as TableChangeToken); lastChangeToken.StringValue = loadedChangeToken.StringValue; } notificationStep = "8"; // Start pulling down the changes bool allChangesRead = false; do { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net if (lastChangeToken == null) { lastChangeToken = new ChangeToken(); // See https://blogs.technet.microsoft.com/stefan_gossner/2009/12/04/content-deployment-the-complete-guide-part-7-change-token-basics/ 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 = notificationSourceList.GetChanges(changeQuery); cc.Load(changes); cc.ExecuteQueryRetry(); notificationStep = "9"; if (changes.Count > 0) { foreach (Change change in changes) { lastChangeToken = change.ChangeToken; if (change is ChangeItem) { // do "work" with the found change DoWork(acc, approvalsList, cc, notificationSourceList, change, notification, log); } } // 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); // Persist the last used changetoken as we'll start from that one // when the next event hits our service if (loadedChangeToken != null) { // Only persist when there's a change in the change token if (!loadedChangeToken.StringValue.Equals(lastChangeToken.StringValue, StringComparison.InvariantCultureIgnoreCase)) { loadedChangeToken.StringValue = lastChangeToken.StringValue; await table.ExecuteAsync(TableOperation.InsertOrReplace(loadedChangeToken)); notificationStep = "10"; } } else { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net var newToken = new TableChangeToken() { PartitionKey = "List", RowKey = id.ToString(), StringValue = lastChangeToken.StringValue }; await table.ExecuteAsync(TableOperation.InsertOrReplace(newToken)); notificationStep = "11"; } #endregion #region "Update" the web hook expiration date when needed // Optionally add logic to "update" the expirationdatetime of the web hook // If the web hook is about to expire within the coming 5 days then prolong it if (notification.ExpirationDateTime.AddDays(-5) < DateTime.Now) { WebHookManager webHookManager = new WebHookManager(); Task <bool> updateResult = Task.WhenAny( webHookManager.UpdateListWebHookAsync( url, listId.ToString(), notification.SubscriptionId, System.Environment.GetEnvironmentVariable("WebHookEndPoint"), DateTime.Now.AddMonths(3), this.accessToken) ).Result; notificationStep = "12"; if (updateResult.Result == false) { throw new Exception(String.Format("The expiration date of web hook {0} with endpoint {1} could not be updated", notification.SubscriptionId, System.Environment.GetEnvironmentVariable("WebHookEndPoint"))); } } #endregion } catch (Exception ex) { // Log error //Console.WriteLine(ex.ToString()); throw new Exception("Step " + notificationStep, ex); } finally { if (cc != null) { cc.Dispose(); } } }