November 28th, 2018

30DaysMSGraph – Day 28 – Use case: Webhooks

List of all posts in the #30DaysMSGraph series

-Today’s post written by Gavin Barron

In Day 27 we created a Team with Microsoft Graph.  Today we’re looking at webhooks in Microsoft Graph.

Overview

Webhooks provide a means for application developers to be notified of changes to data in Microsoft Graph. This allows third party applications to become connected to data held in Microsoft services and provide a richer and more compelling user experience. For example, a web hook could be configured to watch for new emails in a sales person’s inbox and automatically update the contact in the CRM system with any conversations to provide complete account information in the CRM system without needing a user to change context, and in fact provide visibility to the wider organization.

There is another tool provided by Microsoft Graph called Delta Hooks which allows a developer to poll for changes to certain resources.  Which of these tools is right for you will depend on your use cases and the resources you are targeting in Microsoft Graph.

Currently an app can subscribe to changes on the following resources:

  • Messages
  • Events
  • Contacts
  • Users
  • Groups
  • Group conversations
  • Content shared on OneDrive including drives associated with SharePoint sites
  • User’s personal OneDrive folders
  • Security Alerts

A webhook has two main components, a process to create and manage subscriptions, and a process for receiving and processing notifications. These components rely upon each other and need to share some information, as such a true production system will need a persistence system, to ensure smooth operations and avoid issues such as having multiple active subscriptions to the same resource.

To register a subscription the flow is this:

  • Create a new subscription providing at least:
    • The resource being subscribed to
    • The type of changes to be notified of
    • The URL that notifications are to be POSTed to
    • The DateTime at which the subscription will expire (maximum limits listed here)
    • A client state to use to validate future notifications received
  • POST the subscription to Microsoft Graph
  • If the request is valid, Microsoft Graph sends a validation token to the notification URL.
  • The client returns the validation token back to Microsoft Graph within 10 seconds.
  • Microsoft Graph sends a response with the newly created subscription back to the client.
  • Save the subscription into a data store to validate received notifications

Sample solution

The code provided today is for an ASP.NET Core 2.1 Web API. It uses code that has been adapted from the dotnetcore-console-sample repository base console app  and uses the same client secret based authentication code as is used in days 16 through 19. To avoid complexity the sample doesn’t use a real database for persistence but just an in-memory object cache to hold a list of current subscriptions.

In the sample this logic is provided in the SubscriptionsController:

[HttpGet("{upn}")]
public async Task<ActionResult<Subscription>> Get(string upn)
{
    var result = _subscriptionRepository.LoadByUpn(upn);
    if (result != null && result.ExpirationDateTime > DateTime.Now)
    {
        return result;
    }
    string clientState = Guid.NewGuid().ToString("d");
    var request = new Subscription
    {
        ChangeType = "created",
        ExpirationDateTime = DateTime.Now.AddDays(2),
        ClientState = clientState,
        Resource = $"users/{upn}/events",
        NotificationUrl = _notificationUrl.Url
    };
    var response = await _graphClient.PostAsJsonAsync(_subscriptionsResource, request);
    string responseBody = await response.Content.ReadAsStringAsync();

    if (!response.IsSuccessStatusCode)
    {
        Console.WriteLine(response.ReasonPhrase);
        var error = new ObjectResult(responseBody);
        error.StatusCode = 500;
        return error;
    }
    Subscription subscription = JsonConvert.DeserializeObject<Subscription>(responseBody);
    _subscriptionRepository.Save(subscription);
    return subscription;
}

There are a few things to note in this code sample. When the request is first received the existing set of subscriptions is queried to validate that there is not already an active subscription for this user. A new client state is created for each new subscription, this is used when notifications are received to validate that they belong to a subscription created by this system. When a subscription is successfully created in Microsoft graph the subscription is saved into the repository.

 

When receiving and processing notifications:

  • Microsoft Graph POSTs an array of Notification objects to the Notification URL provided when subscribing
  • Each notification contains at least:
    • The Id of the subscription that caused Microsoft Graph to send the notification
    • The resource that triggered the notification
    • The change type that trigged the notification
    • The type of the resource the notification is for
    • The client state corresponding to the subscription
  • Return an HTTP Status Code 202 – Accepted response. If a non-200 class response is received Microsoft Graph will re-send the notification a number of times.

For each notification:

  • Load the corresponding subscription
  • Validate that the supplied and persisted client state values match
  • Load the resource from Microsoft Graph
  • Perform any custom logic that your use case dictates

In the sample this logic is provided in the NotificationsController

[HttpPost]
public async Task<ActionResult> Listen([FromQuery] string validationToken)
{
    if (!string.IsNullOrEmpty(validationToken))
    {
        return Content(validationToken, "plain/text");
    }
    try
    {
        // Read the post body directly as we can't mix optional FromBody and FromQuery parameters
        var postBody = await Request.GetBodyAsync<Notifications>();
        foreach (var item in postBody.value)
        {
            await ProcessEventNotification(item);
        }
    }
    catch (Exception)
    {
        // Just ignore exceptions
    }
    // Send a 202 so MicrosoftGraph knows we processed the notification
    return new StatusCodeResult(202);
}

Here the notifications controller can handle both registration requests which supply a validation token and Notification requests. When handling the notification requests the code will always return and HTTP status of 202 exit did this prevents Microsoft Graph from retrying notification requests. Notifications can be batched combining many notifications into a single request. Given this you should consider what the effects of processing a notification twice or failing to process a notification may be. If either of these cases are problematic, you should look at saving all notifications received into a database. From there the success or failure of the processes that these notification trigger can be tracked. This practice is not shown in the sample for the sake of brevity and clarity.

Permissions

When creating or modifying the Azure Active Directory App Registration the Calendar.Read application permission must be requested and be given admin approval in the same way demonstrated in the day 16 and 17 exercises.

Other tools

When running the Web API project locally it will bind and listen to requests on http://localhost:5000 because this URL is not accessible from Microsoft Graph it is necessary to use a proxy tool like ngrok. Using ngrok you will be able to have an internet address for your code running locally.

Try It Out

Navigate to the dotnetcore-console-sample repo. Clone the repo and configure the project in the Day 28 sub-folder.

Day 28 repo link

  1. Follow the instructions in Day 28 sub-folder to build the project from scratch yourself.

If you run into any issues while building or configuring the project, please create a new Issue on the repo.

 

Join us tomorrow as we manage files in OneDrive with Microsoft Graph in Day 29.