Janeiro de 2018

Volume 33 – Número 1

Escritório - Criar a API para Sua Organização com o Microsoft Graph e o Azure Functions

Por Mike Ammerlaan | Janeiro de 2018

Se a sua organização fosse uma API, como ela seria? 

Você provavelmente começaria com as pessoas (o coração de uma organização) e os tipos de funções e cargos que elas têm. Essas pessoas são normalmente agrupadas em equipes virtuais e definidas que executam tarefas e projetos. Você poria recursos, incluindo onde as pessoas trabalham e as ferramentas usadas nos trabalhos. Em seguida, você adicionaria processos e atividades de trabalho; seriam esses os métodos na API? Embora “widgetMarketingTeam.runCampaign()” talvez seja um pouco simplista demais, com uma API na sua organização, você teria grandes insights sobre como sua organização está funcionando e poderia transformar a produtividade criando processos e ferramentas mais eficientes.

O segredo é disponibilizar cada recurso de maneira consistente e interconectado logicamente, para que você possa criar processos abrangentes que sirvam a como as pessoas e as equipes desejam trabalhar. Quanto mais APIs você puder unir e conectar, mas útil será o conjunto de produtos criados, maior do que a soma das partes.

Para isso, oferecemos o Microsoft Graph, uma API que engloba conjuntos de dados essenciais da sua organização e permite juntar tudo o que é necessário para transformar o modo de execução dos trabalhos. Além disso, o Microsoft Graph trabalha com serviços voltados aos clientes, como OneDrive e Mail (Outlook.com), permitindo a você transformar também sua produtividade pessoal.

Resolvendo o problema do alastramento da API

Em uma organização, o conjunto de sistemas de software usados pode variar bastante. Para os desenvolvedores, cada um apresenta uma estrutura única, normalmente com conjuntos específicos de APIs, requisitos de autenticação e estilos de interação. Muitas vezes, um grande desafio nos projetos de software é unir esses sistemas diferentes para oferecer um nível mais alto de insights, e ele pode incluir a abstração de diferentes APIs e o domínio de esquemas de autenticação individuais.

Historicamente, as APIs individuais de equipes de produto diferentes (no meu caso, na Microsoft) podem funcionar de maneiras diferentes e exigir a integração entre produtos. Mesmo cinco anos atrás, o processo de obter perfil completo e foto de um usuário poderia exigir solicitações tanto a APIs do Exchange (para obter informações sobre alguém) e APIs do SharePoint (para obter uma foto do perfil gerenciado do usuário). Cada uma tinha seus próprios requisitos, sua própria autenticação e seu próprio esquema de API. E se você quisesse obter informações sobre o gerente de alguém? Isso envolvia consultar um terceiro sistema para obter o organograma. Essas operações podiam ser unidas, mas eram mais complexas do que o necessário.

O Microsoft Graph nasceu da necessidade de resolver esse problema. Ao unificar dados e autenticação e tornar o sistemas consistentes, o conjunto de APIs ficam mais fáceis e práticos de se usar. O Microsoft Graph une diversos sistemas em toda a sua organização, representando facetas e funções essenciais em uma empresa. Desde o seu lançamento, há dois anos, o Microsoft Graph continuou a se expandir em funções e recursos até poder servir como uma API fundacional para sua organização.

A base do Microsoft Graph é o conjunto de usuários, normalmente todos os funcionários com uma conta da organização. Grupos centralizados e simplificados são um conceito emergente no Microsoft Graph, normalmente começando com uma lista de usuários e outros grupos de segurança. Os grupos podem ter um conjunto associado de recursos, como um espaço de trabalho baseado em chat das Equipes da Microsoft, um painel de tarefas do Planner e um site do SharePoint com bibliotecas e arquivos de documentos. Daí, várias ferramentas de trabalho estão representadas para usuários e grupos, incluindo arquivos pela API do Drive, tarefas pela API do Planner, emails de entrada para usuários e grupos, contatos, calendário e muito mais, como mostra a Figura 1.

APIs de produtividade do Microsoft Graph
Figura 1 APIs de produtividade do Microsoft Graph

Ao longo do tempo, novos recursos foram adicionados às APIs no Microsoft Graph. Uma nova capacidade de persistir metadados personalizados com itens no Microsoft Graph permite a personalização detalhada desses itens. Agora, um grupo não é mais apenas um grupo; com metadados adicionais que descrevem tópico, instrutor e tempo, um grupo pode representar uma turma em uma instituição de ensino. Você poderia usar esses metadados para fazer consultas, por exemplo, encontrar todos os grupos que representam turmas de ciências. Ou você poderia conectar seus sistemas no Microsoft Graph adicionando identificadores do seu sistema às entidades relacionadas no Microsoft Graph.

O Microsoft Graph também vai além de oferecer APIs CRUD (criar, ler, atualizar e excluir) para objetos principais. Um grande recurso é a camada de insights gerados nos bastidores durante o trabalho dos usuários. Por exemplo, embora o Graph contenha uma organograma e uma coleção de grupos completos, nem sempre eles formam a melhor representação de como as equipes trabalham. Com a análise dos trabalhos, você pode obter uma lista das pessoas que se relacionam com mais frequência (equipes virtuais) e arquivos com que um usuário pode estar conectado. Além disso, utilitários comuns, como os usados para encontrar horários disponíveis dos usuários para uma reunião, são disponibilizados como métodos.

Azure Functions

O Microsoft Graph existe para ser usado e personalizado em sistemas e processos mais amplos. Como uma API REST simples e em conjunto com uma ampla gama de SDKs, o Microsoft Graph é projetado para ter um uso simples e direto. Uma opção natural para criar processos e integrações no Microsoft Graph é o Azure Functions (functions.azure.com), que deixa você adicionar blocos específicos de código onde for necessário pagando apenas incrementalmente pelo código conforme o uso. O Azure Functions dá suporte a desenvolvimento entre linguagens, incluindo C# e Node.js.

Recentemente, um novo conjunto de integrações com o Azure Functions facilita a conexão com o Microsoft Graph. O Azure Functions Binding Extensions, agora disponível em versão prévia com o tempo de execução do Azure Functions 2.0, automatiza algumas tarefas comuns no trabalho com o Microsoft Graph, incluindo autenticação e funcionamento com a mecânica dos webhooks.

Vamos dar uma olhada em um exemplo para começar a trabalhar com o Microsoft Graph.

Criando tarefas com o Azure Functions

Imagine que você quisesse fazer com que os gerentes revisassem e aprovassem uma ação executada por um membro da equipe. As tarefas de usuário são uma forma de pedir aos usuários que realizem uma ação; para converter e rastrear a ação humana. Nesse caso, desejo implementar um serviço Web simples que criará uma tarefa atribuída ao gerente de um usuário. 

A primeira parada em qualquer projeto do Microsoft Graph é, normalmente, o Explorador do Graph. O Explorador do Graph é um site de aplicativo que permite modelar chamadas do Microsoft Graph rapidamente, explorar seus resultados e conceber totalmente o que é possível fazer. Disponível em developer.microsoft.com/graph, o Explorador do Graph permite usar uma locação de demonstração somente leitura ou entrar na sua própria locação. Você pode entrar com a conta da sua organização e acessar diretamente seus próprios dados. Recomendamos usar uma locação de desenvolvedor disponível no Programa de Desenvolvedor do Office em dev.office.com/devprogram. Isso dará uma locação diferente em que você poderá ter a liberdade de experimentar no seu desenvolvimento.

Nesse caso, você pode inserir duas URLs simples para ver o tipo de chamadas que você fará nesse exemplo. Primeiro, marque “obter o gerente de um usuário”, que pode ser visto no Explorador do Graph selecionando o exemplo “OBTER meu gerente” mostrado na Figura 2. A URL por trás disso é mostrada no campo Executar Consulta.

Resultados da opção OBTER meu gerente
Figura 2 Resultados da opção OBTER meu gerente

A segunda parte da operação é criar uma tarefa do Planner. No Explorador do Graph, você pode expandir o conjunto de exemplos para adicionar exemplos de tarefas do Planner. Com esse conjunto de exemplo, você pode ver a operação para criar uma tarefa do Planner (um POST para https://graph.microsoft.com/v1.0/planner/tasks).

Agora que você entende as solicitações de serviço Web envolvidas, pode criar uma função usando o Azure Functions.

Para começar, crie um novo aplicativo Azure Functions. Normalmente, siga as instruções em aka.ms/azfnmsgraph para isso. Resumindo, porque o novo recurso Azure Functions Binding Extensions está em versão prévia, você precisará mudar seu aplicativo Azure Functions para o tempo de execução da versão prévia (“beta”) do 2.0. Você também precisará instalar a Extensão do Microsoft Graph e configurar a Autenticação do Serviço de Aplicativo.

Ao configurar o Registro de Aplicativo do Microsoft Graph, você precisará adicionar algumas permissões adicionais a esse exemplo a fim de dar suporte à leitura das informações e tarefas do gerente, incluindo:

  • CRUD tarefas e projetos do usuário (Tasks.ReadWrite)
  • Ver o perfil básico do usuário (perfil)
  • Ler e gravar todos os grupos (Group.ReadWrite.All)
  • Ler o perfil básico de todos os usuários (User.ReadBasic.All)

É ideal aproveitar o Azure Functions Binding Extensions no Microsoft Graph para lidar com a autenticação e fazer com que você tenha um token de acesso autenticado para acessar APIs do Microsoft Graph. Para isso, crie um gatilho C# HTTP padrão. Em Integrar, selecione o Editor Avançado e use as associações mostradas na Figura 3. Isso exige que o usuário entre, autentique e aprove o aplicativo antes de poder usá-lo.

Figura 3 Criando um gatilho HTTP para tratar da autenticação

{
  "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
}

O código para a função é mostrado na Figura 4. Observe que você precisará configurar uma variável de ambiente para o aplicativo de função chamado PlanId, que tem o identificador do Plano do Planner que deve ser usado nas tarefas. Isso pode ser feito nas Configurações do Aplicativo do aplicativo de funções.

Figura 4 Postando uma tarefa atribuída à fonte do Azure Functions do gerente de um usuário

#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;
}

Esse exemplo mostra como você pode unir conjuntos de dados diferentes (tarefas do gerente de um usuário e do Planner, nesse caso) em uma parte do código com um token de autenticação. A criação e a atribuição de tarefas é uma maneira comum de gerar atividades entre equipes e, portanto, a capacidade de criar tarefas imediatamente e aproveitar experiências do Planner existentes é bastante útil. Não é bem “widgetMarketingTeam.launchCampaign()”, mas pelo menos você pode ver como poderia criar o conjunto inicial de tarefas para começar os trabalhos da equipe de maneira estruturada.

Processando arquivos no OneDrive

Outra tarefa que pode ser executada é o processamento de arquivos que existem no OneDrive de um usuário. Nesse caso, você aproveita o Azure Functions Binding Extensions no Microsoft Graph para fazer o trabalho de preparar um arquivo para uso. Em seguida, você transmite para as APIs de Serviços Cognitivos a fim de fazer reconhecimento de voz. Esse é um exemplo de processamento de dados que pode ser útil para obter mais valor dos arquivos no OneDrive e no SharePoint.

Para começar, siga alguma das etapas do exemplo anterior, incluindo configurar um Aplicativo de Funções e um registro do Azure Active Directory. Observe que o aplicativo do Azure Active Directory usado para esse exemplo precisará ter a permissão “Ler todos os arquivos que o usuário pode acessar” (Files.Read.All). Você também precisará de uma chave de Speech API dos Serviços Cognitivos, que pode ser obtida em aka.ms/tryspeechapi.

Novamente, inicie com o Azure Functions Binding Extensions e configure um novo gatilho C# HTTP. Na guia Integrar da sua função, use a marcação de associação mostrada na Figura 5 para conectar sua função a uma extensão de associação. Nesse caso, a extensão de associação vincula o parâmetro myOneDriveFile na sua função do Azure à extensão de associação onedrive.

Figura 5 Configurando um novo gatilho para obter um arquivo no 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
}

Agora é a hora do código, mostrado na Figura 6.

Figura 6 Transcrevendo um arquivo de áudio do OneDrive

#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();
  }
}

Com essa função, depois que um usuário entrar na sua função do Azure, ele pode especificar um parâmetro de nome de arquivo. Se o arquivo tem um nome .WAV e conteúdo em inglês, ele será transcrito em texto em inglês. Como isso é implementado com o Azure Functions, sua função vai incorrer em custos somente quando for chamada, oferecendo uma maneira flexível de estender os dados que você tem no Microsoft Graph.

Azure Functions + Microsoft Graph

Os dois exemplos apresentados aqui mostram como você pode criar tanto processos humanos quanto técnicos com dados no Microsoft Graph. Combinado com a abrangência de cobertura do Microsoft Graph e a capacidade de cruzar cargas de trabalho (por exemplo, organogramas e tarefas, como foi o caso do exemplo de tarefa deste artigo), você pode criar e agregar valor a toda a sua organização. A combinação do Microsoft Graph com o Azure Functions permite criar a API completa para a sua organização e transformar a produtividade de todos. Comece a criar soluções para a sua organização visitando developer.microsoft.com/graph e trabalhando com o Azure Functions em functions.azure.com.


Mike Ammerlaan é diretor de marketing de produtos na equipe Microsoft Office Ecosystem, ajudando as pessoas a criarem soluções envolventes com o Office 365. Antes disso, ele trabalhou na Microsoft como gerente de programa por 18 anos, desenvolvendo produtos como SharePoint, Excel, Yammer, Bing Maps e Combat Flight Simulator.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo:  Ryan Gregg, Matthew Henderson e Dan Silver


Discuta esse artigo no fórum do MSDN Magazine