January 2018

Volume 33 Number 1

[Office]

Build the API to Your Organization with Microsoft Graph and Azure Functions

By Mike Ammerlaan | January 2018

If you think of your organization as an API, what would it look like? 

You’d probably start with the people—the heart of an organization—and the kinds of roles and functions they fulfill. Such people are frequently grouped into defined and virtual teams that accomplish tasks and projects. You’d layer on resources, including where people work and the tools they use to get work done. You’d then add processes and work activities—perhaps these are methods in the API? Though “widgetMarketingTeam.runCampaign()” is perhaps wildly overly simplistic, nevertheless, with an API to your organization, you’d get great insights into how your organization is functioning and could transform productivity by building more efficient processes and tools.

The key is making every resource available consistently, and inter­connected logically, so you can craft comprehensive processes to fit the way individuals and teams want to work. The more APIs you can bring together and connect, the more useful by far the net set of products you build could be—greater than the sum of its parts.

For this, we offer Microsoft Graph—an API that spans key data sets within your organization and allows you to pull together everything you need to transform how work is performed. Moreover, Microsoft Graph works with consumer services, such as OneDrive and Mail (Outlook.com), empowering you to transform personal productivity, as well.

Solving the Problem of API Sprawl

Across an organization, the set of software systems in use can vary wildly. For developers, each presents a unique structure, typically with a distinct set of APIs, authentication requirements and style of interaction. Frequently, a major challenge of software projects is simply bridging these different systems to provide a higher level of insight and can include abstracting out different APIs and mastering individual authentication schemes.

Historically, individual APIs from different product teams—at Microsoft, in my case—would work differently and require cross-product integration. Even five years ago, the process of getting a user’s complete profile and photo would require callouts to both Exchange APIs (to get information about a person) and SharePoint APIs (to get a photo from a user’s managed profile). Each had their own authentication, API scheme, and differing requirements. What if you then wanted to get the information about a person’s manager? That would involve querying a third system to get organizational hierarchy. These operations were all possible to pull together, but more complex than they needed to be.

Microsoft Graph was born out of a desire to solve this problem. By unifying data and authentication, and making systems consistent, the net set of APIs becomes much easier and more practical to use. Microsoft Graph pulls together diverse systems from across your organization, representing key facets and functions in a company. Since its launch two years ago, Microsoft Graph has continued to grow in breadth of both functionality and capability, to where it really can serve as a foundational API for your organization.

At the core of Microsoft Graph is the set of users—typically all employees with an account in an organization. Simplified, centralized groups are an emerging concept in Microsoft Graph, generally starting with a list of users and other security groups. Groups can have an associated set of resources, like a Microsoft Teams chat-based workspace, a Planner task board, and a SharePoint site with document libraries and files. From there, various tools of work are represented for users and groups, including files via the Drive API, tasks via the Planner API, incoming mail for both users and groups, contacts, calendar, and more, as shown in Figure 1.

Productivity APIs of Microsoft Graph
Figure 1 Productivity APIs of Microsoft Graph

Over time, new capabilities have been added across the APIs in Microsoft Graph. A new ability to persist custom metadata along with items in Microsoft Graph gives you the ability to deeply customize these items. Now a group is no longer just a group—with additional metadata describing topic, instructor and timing, a group could represent a class in an educational institution. You could use this metadata to then perform queries—for example, find all groups that represent science classes. Alternatively, you could connect your systems into Microsoft Graph by adding identifiers from your system to the related entities within Microsoft Graph.

Microsoft Graph also goes beyond providing create, read, update and delete (CRUD) APIs for core objects. A major feature is a layer of insights that are generated behind the scenes as users work. For example, though Graph contains a full organizational hierarchy and collection of groups, these may not always form the best representation of how teams work. Through an analysis of work, you can get a list of the most closely related people (virtual teams) and files with which a user might be connected. In addition, common utilities, such as those for finding an available meeting time among a set of users, are made available as methods.

Azure Functions

Microsoft Graph exists to be used and customized in broader systems and processes. As a simple REST API and coupled with a wide array of SDKs, Microsoft Graph is designed to be straightforward to work with. A natural choice for building processes and integrations in Microsoft Graph is Azure Functions (functions.azure.com), which lets you add pinpointed blocks of code where you need it while only paying incrementally for code as it’s used. Azure Functions supports development across languages, including C# and Node.js.

Recently, a new set of integrations with Azure Functions makes it easier to connect to Microsoft Graph. Azure Functions Binding Extensions, now available in preview with the Azure Functions 2.0 runtime, automates some of the common tasks of working with Microsoft Graph, including authentication and working with the mechanics of webhooks.

Let’s take a look at an example to get started in working with Microsoft Graph.

Creating Tasks via Azure Functions

Imagine you’d like to have managers review and approve an action undertaken by a member of their team. User tasks are one way to ask users to perform an action—to convert and track human action. In this case, I want to implement a simple Web service that will create a task assigned to a user’s manager. 

The first stop in any Microsoft Graph project is usually the Graph Explorer. Graph Explorer is an application Web site that lets you quickly model Microsoft Graph calls, explore their results, and fully conceive of all you might do. Available from developer.microsoft.com/graph, the Graph Explorer lets you either use a read-only demo tenancy, or sign into your own tenancy. You can sign in with your organization account and directly access your own data. We recommend using a developer tenancy, available from the Office Developer Program at dev.office.com/devprogram. This will give you a separate tenancy where you can feel free to experiment with your development.

In this case, you can enter two simple URLs to see the kind of calls you’ll be making in this sample. First, you want to check “get a user’s manager,” which you can see in Graph Explorer by selecting the “GET my manager” sample, shown in Figure 2. The URL behind this is shown in the Run Query field.

Results of Selecting GET my manager
Figure 2 Results of Selecting GET my manager

The second part of the operation is to create a Planner Task. Within Graph Explorer, you can expand the set of samples to add samples of Planner tasks. Within this sample set, you can see the operation for creating a Planner Task (a POST to https://graph.microsoft.com/v1.0/planner/tasks).

Now that you understand the Web service requests involved, you can build a function using Azure Functions.

To get started, create a new Azure Functions application. In general, you’ll want to follow the instructions at aka.ms/azfnmsgraph to get this accomplished. In brief, because the new Azure Functions Binding Extensions capability is in Preview, you’ll need to switch your Azure Functions application over to the 2.0 preview (“beta”) runtime. You’ll also need to install the Microsoft Graph Extension, as well as configure App Service Authentication.

As you configure the Microsoft Graph Application Registration, for this sample you’ll need to add some further permissions to support reading manager information and tasks, including:

  • CRUD user tasks and projects (Tasks.ReadWrite)
  • View users’ basic profile (profile)
  • Read and Write All Groups (Group.ReadWrite.All)
  • Read all users’ basic profile (User.ReadBasic.All)

You’ll want to leverage Azure Functions Binding Extensions for Microsoft Graph to handle the authentication and ensure you have an authenticated access token by which you can access Microsoft Graph APIs. To do this, you’ll create a standard HTTP C# trigger. Under Integrate, select the Advanced Editor and use the bindings shown in Figure 3. This will require that the user sign in, authenticate and approve your application before it can be used.

Figure 3 Creating an HTTP Trigger to Handle Authentication

{
  "bindings": [
    {
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "type": "token",
      "direction": "in",
      "name": "accessToken",
      "resource": "https://graph.microsoft.com",
      "identity": "userFromRequest"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ],
  "disabled": false
}

The code for the function is shown in Figure 4. Note that you’ll need to configure an environment variable for the function application called PlanId, which has the identifier of the Planner Plan you wish to use for your tasks. This can be done via Application Settings for the function application.

Figure 4 Posting a Task Assigned to a User’s Manager Azure Functions Source

#r "Newtonsoft.Json"
using System.Net;
using System.Threading.Tasks;
using System.Configuration;
using System.Net.Mail;
using System.IO;
using System.Web;
using System.Text;
using Newtonsoft.Json.Linq;
public static HttpResponseMessage Run(HttpRequestMessage req, string accessToken, TraceWriter log)
{
  log.Info("Processing incoming task creation requests.");
  // Retrieve data from query string
  // Expected format is taskTitle=task text&taskBucket=bucket
  // title&taskPriority=alert
  var values = HttpUtility.ParseQueryString(req.RequestUri.Query);
  string taskTitle = values["taskTitle"];
  string taskBucket = values["taskBucket"];
  string taskPriority = values["taskPriority"];
  if (String.IsNullOrEmpty(taskTitle))
  {
    log.Info("Incomplete request received - no title.");
    return new HttpResponseMessage(HttpStatusCode.BadRequest);
  }
  string planId = System.Environment.GetEnvironmentVariable("PlanId");
  // Retrieve the incoming users' managers ID
  string managerJson = GetJson(
    "https://graph.microsoft.com/v1.0/me/manager/", accessToken, log);
    dynamic manager = JObject.Parse(managerJson);
  string managerId = manager.id;
  string appliedCategories = "{}";
  if (taskPriority == "alert" || taskPriority == "1")
  {
    appliedCategories = "{ \"category1\": true }";
  }
  else
  {
    appliedCategories = "{ \"category2\": true }";
  }
  string now =  DateTime.UtcNow.ToString("yyyy-MM-ddTHH\\:mm\\:ss.fffffffzzz");
  string due =  DateTime.UtcNow.AddDays(5).ToString(
    "yyyy-MM-ddTHH\\:mm\\:ss.fffffffzzz");
  string bucketId = "";
  // If the incoming request wants to place a task in a bucket,
  // find the bucket ID to add it to
  if (!String.IsNullOrEmpty(taskBucket))
  {
    // Retrieve a list of planner buckets so that you can match
    // the task to a bucket, where possible
    string bucketsJson = GetJson(
      "https://graph.microsoft.com/v1.0/planner/plans/" + planId +
      "/buckets", accessToken, log);
    if (!String.IsNullOrEmpty(bucketsJson))
    {
      dynamic existingBuckets = JObject.Parse(bucketsJson);
      taskBucket = taskBucket.ToLower();
      foreach (var bucket in existingBuckets.value)
      {
        var existingBucketTitle = bucket.name.ToString().ToLower();
        if (taskBucket.IndexOf(existingBucketTitle) >= 0)
        {
          bucketId = ", \"bucketId\": \"" + bucket.id.ToString() + "\"";
        }
      }
    }
  }
  string jsonOutput = String.Format(" {{ \"planId\": \"{0}\", \"title\": \"{1}\", \"orderHint\": \" !\", \"startDateTime\": \"{2}\", \"dueDateTime\": \"{6}\", \"appliedCategories\": {3}, \"assignments\": {{ \"{4}\": {{ \"@odata.type\": \"#microsoft.graph.plannerAssignment\",  \"orderHint\": \" !\"  }} }}{5} }}",
    planId, taskTitle, now, appliedCategories, managerId, bucketId, due);
  log.Info("Creating new task: " + jsonOutput);
  PostJson("https://graph.microsoft.com/v1.0/planner/tasks",
    jsonOutput, accessToken, log);
  return new HttpResponseMessage(HttpStatusCode.OK);
}
private static string GetJson(string url, string token, TraceWriter log)
{
  HttpWebRequest hwr = (HttpWebRequest)WebRequest.CreateHttp(url);
  log.Info("Getting Json from endpoint '" + url + "'");
  hwr.Headers.Add("Authorization", "Bearer " + token);
  hwr.ContentType = "application/json";
  WebResponse response = null;
  try
  {
    response = hwr.GetResponse();
    using (Stream stream = response.GetResponseStream())
    {
      using (StreamReader sr = new StreamReader(stream))
      {
        return sr.ReadToEnd();
      }
     }
  }
  catch (Exception e)
  {
    log.Info("Error: " + e.Message);
  }
  return null;
}
private static string PostJson(string url, string body, string token, TraceWriter log)
{
  HttpWebRequest hwr = (HttpWebRequest)WebRequest.CreateHttp(url);
  log.Info("Posting to endpoint " + url);
  hwr.Method = "POST";
  hwr.Headers.Add("Authorization", "Bearer " + token);
  hwr.ContentType = "application/json";
  var postData = Encoding.UTF8.GetBytes(body.ToString());
  using (var stream = hwr.GetRequestStream())
  {
  stream.Write(postData, 0, postData.Length);
  }
  WebResponse response = null;
  try
  {
    response = hwr.GetResponse();
    using (Stream stream = response.GetResponseStream())
    {
      using (StreamReader sr = new StreamReader(stream))
      {
        return sr.ReadToEnd();
      }
    }
  }
  catch (Exception e)
  {
    log.Info("Error: " + e.Message);
  }
  return null;
}

This sample shows how you can pull together disparate sets of data (a user’s manager and Planner tasks in this case) in one piece of code with one authentication token. Creating and assigning tasks is a common way to drive activities across teams, so the ability to create tasks on the fly and leverage existing Planner experiences is quite useful. It’s not quite “widgetMarketingTeam.launchCampaign()”—but at least you can see how you’d create the starter set of tasks that would get the team off to a focused, structured start.

Processing Files in OneDrive

Another task you can perform is to process files that exist on a user’s OneDrive. In this instance, you take advantage of Azure Functions Binding Extensions for Microsoft Graph to do the work of preparing a file for use. You then pass it into Cognitive Services APIs for doing voice recognition. This is an example of data processing that can be a useful way to get more value out of files across OneDrive and SharePoint.

To get started, you follow some of the same steps as in the previous example, including setting up a Function App and an Azure Active Directory registration. Note that the Azure Active Directory application registration that you use for this sample will need to have the “Read all files that user can access” (Files.Read.All) permission. You’ll also need to have a Cognitive Services Speech API key, which you can obtain from aka.ms/tryspeechapi.

As before, start with Azure Functions Binding Extensions and set up a new HTTP C# trigger. Under the Integrate tab of your function, use the binding markup shown in Figure 5 to connect your function to a binding extension. In this case, the binding extension ties the myOneDriveFile parameter in your Azure function to the onedrive binding extension.

Figure 5 Setting Up a New Trigger for Getting a File on OneDrive

{
  "bindings": [
    {
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "myOneDriveFile",
      "type": "onedrive",
      "direction": "in",
      "path": "{query.filename}",
      "identity": "userFromRequest",
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ],
  "disabled": false
}

Now, it’s time for the code, which is shown in Figure 6.

Figure 6 Transcribing an Audio File from One Drive

#r "Newtonsoft.Json"
using System.Net;
using System.Text;
using System.Configuration;
using Newtonsoft.Json.Linq;
public static  async Task<HttpResponseMessage> Run(HttpRequestMessage req,
  Stream myOneDriveFile, TraceWriter log)
{
  // Download the contents of the audio file
  log.Info("Downloading audio file contents...");
  byte[] audioBytes;
  audioBytes = StreamToBytes(myOneDriveFile);
  // Transcribe the file using cognitive services APIs
  log.Info($"Retrieving the cognitive services access token...");
  var accessToken =
    System.Environment.GetEnvironmentVariable("SpeechApiKey");
  var bingAuthToken = await FetchCognitiveAccessTokenAsync(accessToken);
  log.Info($"Transcribing the file...");
  var transcriptionValue = await RequestTranscriptionAsync(
    audioBytes, "en-us", bingAuthToken, log);
  HttpResponseMessage hrm = new HttpResponseMessage(HttpStatusCode.OK);
  if (null != transcriptionValue)
  {
    hrm.Content = new StringContent(transcriptionValue, Encoding.UTF8, "text/html");
  }
  else
  {
    hrm.Content = new StringContent("Content could not be transcribed.");
  }
  return hrm;
}
private static async Task<string> RequestTranscriptionAsync(byte[] audioBytes,
  string languageCode, string authToken, TraceWriter log)
{
  string conversation_url = $"https://speech.platform.bing.com/speech/recognition/conversation/cognitiveservices/v1?language={languageCode}";
  string dictation_url = $"https://speech.platform.bing.com/speech/recognition/dictation/cognitiveservices/v1?language={languageCode}";
  HttpResponseMessage response = null;
  string responseJson = "default";
  try
  {
    response = await PostAudioRequestAsync(conversation_url, audioBytes, authToken);
    responseJson = await response.Content.ReadAsStringAsync();
    JObject data = JObject.Parse(responseJson);
    return data["DisplayText"].ToString();
  }
  catch (Exception ex)
  {
    log.Error($"Unexpected response from transcription service A: {ex.Message} |" +
      responseJson + "|" + response.StatusCode  + "|" +
      response.Headers.ToString() +"|");
    return null;
  }
}
private static async Task<HttpResponseMessage> PostAudioRequestAsync(
  string url, byte[] bodyContents, string authToken)
{
  var payload = new ByteArrayContent(bodyContents);
  HttpResponseMessage response;
  using (var client = new HttpClient())
  {
    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + authToken);
    payload.Headers.TryAddWithoutValidation("content-type", "audio/wav");
    response = await client.PostAsync(url, payload);
  }
  return response;
}
private static byte[] StreamToBytes(Stream stream)
{
  using (MemoryStream ms = new MemoryStream())
  {
    stream.CopyTo(ms);
    return ms.ToArray();
  }
}
private static async Task<string> FetchCognitiveAccessTokenAsync(
  string subscriptionKey)
{
  string fetchUri = "https://api.cognitive.microsoft.com/sts/v1.0";
  using (var client = new HttpClient())
  {
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
    UriBuilder uriBuilder = new UriBuilder(fetchUri);
    uriBuilder.Path += "/issueToken";
    var response = await client.PostAsync(uriBuilder.Uri.AbsoluteUri, null);
    return await response.Content.ReadAsStringAsync();
  }
}

With this function in place, after a user has signed into their Azure function, they can specify a filename parameter. If a file has a .WAV filename and contains English content within it, this will get transcribed into English text. Because this is implemented with Azure Functions, your function will typically incur cost only as it’s called, providing a flexible way to extend the data you have in Microsoft Graph.

Azure Functions + Microsoft Graph

The two samples I presented here show how you can build both human and technical processes on top of data within Microsoft Graph. Combined with the breadth of coverage of Microsoft Graph and the ability to cross workloads (for example, organizational hierarchy and tasks, as was the case with the task sample in this article), you can build and add value across your entire organization. Combining Microsoft Graph and Azure Functions allows you to build out the full API to your organization, and transform productivity for all. Get started in building solutions for your organization by visiting developer.microsoft.com/graph, and working with Azure Functions at functions.azure.com.


Mike Ammerlaan is a director of product marketing on the Microsoft Office Ecosystem team, helping people build engaging solutions with Office 365. Prior to this, he worked at Microsoft as a program manager for 18 years, developing products such as SharePoint, Excel, Yammer, Bing Maps and Combat Flight Simulator.

Thanks to the following Microsoft technical experts for reviewing this article:  Ryan Gregg, Matthew Henderson and Dan Silver


Discuss this article in the MSDN Magazine forum