Março de 2018

Volume 33 – Número 3

Pontos de Dados: Chamar os Azure Functions da Plataforma Universal do Windows

Por Julie Lerman

Leia toda a série EF Core 2 e dados de aplicativo UWP:

Uso do EF Core 2 e dos Azure Functions para armazenar dados de aplicativo UWP de forma local e global, Parte 1
Uso do EF Core 2 e dos Azure Functions para armazenar dados de aplicativo UWP de forma local e global, Parte 2
Uso do EF Core 2 e dos Azure Functions para armazenar dados de aplicativo UWP de forma local e global, Parte 3
Uso do EF Core 2 e dos Azure Functions para armazenar dados de aplicativo UWP de forma local e global, Parte 4

Julie LermanEsta é a parte final da minha série sobre a criação de um aplicativo UWP (Plataforma Universal do Windows) que armazena dados localmente e na nuvem. Na primeira parte, criei o jogo CookieBinge UWP, que usa o Entity Framework Core 2 (EF Core 2) para armazenar pontuações de jogo no dispositivo em que o jogo está sendo jogado. Nas duas partes seguintes, mostrei como criar Azure Functions na nuvem para armazenar as pontuações de jogo e recuperá-las de um banco de dados do Microsoft Azure Cosmos DB. Por fim, nesta coluna, você verá como fazer solicitações desde um aplicativo UWP para os Azure Functions para enviar pontuações, bem como receber e exibir as principais pontuações do jogador em todos os seus dispositivos, assim como as principais pontuações de todos os jogadores no mundo.

Esta solução também permite que os usuários se registrem na nuvem e vinculem qualquer dispositivo ao registro. Embora eu aproveite as informações desse registro para enviar e recuperar pontuações dos Azure Functions, não descreverei essa parte do aplicativo. Entretanto, você pode ver o código no download, incluindo o código do novos Azure Functions que criei para o registro.

Onde paramos

Uma pequena atualização ajudará você a se familiarizar novamente com o aplicativo UWP e os Azure Functions que serão conectados.

No jogo CookieBinge, quando um usuário conclui uma comilança, tem dois botões para dizer ao aplicativo que já terminou. Um é o botão “Worth it” (Vale a pena) para indicar que já acabou e que está feliz com todos os cookies que comeu. O outro é o botão “Not worth it” (Não vale a pena). Em resposta a esses eventos de clique, a lógica leva ao método BingeServices.RecordBinge, que usa o EF Core 2 para armazenar os dados no banco de dados local.

O aplicativo também tem um recurso que exibe as cinco maiores pontuações presentes no banco de dados local.

Há três Azure Functions inseridos em minhas últimas colunas. O primeiro recebe a pontuação do jogo, além da UserId do jogador (atribuída pelo recurso de registro) e o nome do dispositivo no qual o jogo foi executado. Então, a função armazena esses dados no banco de dados do Cosmos DB. As duas outras funções respondem a solicitações de dados do banco de dados do Cosmos DB. Uma recebe a UserId do jogador e retorna as cinco maiores pontuações enviadas de todos os dispositivos em que ele joga. A outra simplesmente retorna as cinco maiores pontuações armazenadas no banco de dados, independentemente do jogador.

Assim, agora a tarefa é integrar os Azure Functions ao jogo. Primeiro, no momento em que a pontuação do jogo é armazenada no banco de dados local, o aplicativo deverá enviar também a pontuação e outros dados relevantes à função StoreScore. Segundo, no momento em que o aplicativo lê o histórico de pontuação do banco de dados local, também deve enviar solicitações para as funções que retornam as pontuações e exibir os resultados dessas solicitações, como mostrado na Figura 1.

Pontuações do banco de dados do Azure Cosmos DB recuperadas dos Azure Functions
Figura 1 Pontuações do banco de dados do Azure Cosmos DB recuperadas dos Azure Functions

Comunicação com a Web do UWP

A estrutura UWP usa um conjunto especial de APIs para fazer solicitações da Web e receber respostas. Na verdade, há duas opções de namespace e eu recomendo a leitura da postagem de blog do MSDN: “Demystifying HttpClient APIs in the Universal Windows Platform” (Desmistificando as APIs HttpClient na Plataforma Universal do Windows), em bit.ly/2rxZu3f. Trabalharei com as APIs no namespace Windows.Web.Http. Essas APIs têm um requisito muito específico para a forma como os dados são enviados junto com as solicitações, e isso significa um pequeno esforço extra. Para casos em que preciso enviar JSON junto com a minha solicitação, aproveitarei uma classe auxiliar, HttpJsonContent, que combinará o conteúdo JSON ao conteúdo do cabeçalho e executará alguma lógica adicional. HttpJsonContent exige que eu envie meu JSON no formato de um Windows.Data.Json.JsonValue. Assim, você verá onde utilizei essas etapas. Como alternativa, eu poderia definir explicitamente o conteúdo do cabeçalho na HttpRequest e então postar como uma String usando o método StringContent, como demonstrado em bit.ly/2BBjFNE.

Eu encapsulei toda a lógica para interação com os Azure Functions em uma classe chamada CloudService.cs.

Assim que percebi o padrão para o uso dos métodos de solicitação do UWP e como criar objetos JsonValue, criei um método chamado CallCookieBingeFunctionAsync, que encapsula a maior parte da lógica. O método tem dois parâmetros: o nome do Azure Function a ser chamado e um objeto JsonValue. Também criei uma sobrecarga desse método que não exige o parâmetro de objeto JsonValue.

Veja a assinatura para esse método:

private async Task<T> CallCookieBingeFunctionAsync<T>(string apiMethod,
  JsonValue jsonValue)

Como existem três Azure Functions diferentes que preciso chamar, vamos começar pelo mais simples — GetTop5GlobalUserScores. Essa função não usa parâmetros ou outro conteúdo e retorna resultados como JSON.

O método GetTopGlobalScores na classe CloudService chama meu método CallCookieBingeFunctionAsync, passando o nome da função, e então retorna os resultados contidos na resposta da função.

public async Task<List<ScoreViewModel>> GetTopGlobalScores()
{
  var results= await CallCookieBingeFunctionAsync<List<ScoreViewModel>>
    ("GetTop5GlobalUserScores");
  return results;
}

Observe que não estou passando um segundo parâmetro para o método. Isso significa que a sobrecarga que criei e que não exige um JsonValue será chamada:

private async Task<T> CallCookieBingeFunctionAsync<T>(string apiMethod)
{
  return await CallCookieBingeFunctionAsync<T>(apiMethod, null);
}

Por sua vez, isso chama a outra versão do método e simplesmente passa um nulo onde o JsonValue é esperado. Veja a listagem completa do método CallCookieBingeFunctionAsync (que definitivamente precisa de uma explicação):

private async Task<T> CallCookieBingeFunctionAsync<T>(string apiMethod, JsonValue jsonValue)
{
  var httpClient = new HttpClient();
  var uri = new Uri("https://cookiebinge.azurewebsites.net/api/" + apiMethod);
  var httpContent = jsonValue != null ? new HttpJsonContent(jsonValue): null;
  var cts = new CancellationTokenSource();
  HttpResponseMessage response = await httpClient.PostAsync(uri,
    httpContent).AsTask(cts.Token);  string body = await response.Content.ReadAsStringAsync();
  T deserializedBody = JsonConvert.DeserializeObject<T>(body);
  return deserializedBody;
}

Na primeira etapa, o método cria uma instância de Windows.Web.Http.HttpClient. Então, o método constrói as informações necessárias para criar a solicitação do HttpClient, começando pelo endereço Web da função a ser chamada. Todas as minhas funções começarão com https://cookiebinge.azurewebsites.net/­api/, então codifiquei esse valor no método e então anexei o nome da função passada para o método.

Em seguida, eu preciso definir os cabeçalhos e todo o conteúdo passado para a função. Como expliquei anteriormente, escolhi o uso da classe auxiliar, HttpJsonContent, para esta etapa. Copiei essa classe da seção JSON das amostras oficiais Universais do Windows (bit.ly/2ry7mBP), que me forneceram um meio de transformar um objeto JsonValue em um objeto que implementa IHttpContent. (Veja a classe completa que copiei no download). Se nenhum JsonValue for passado para o método, como é o caso da função GetTop5GlobalUserScores, a variável httpContent será nula.

A próxima etapa no método define uma CancellationTokenSource na variável cts. Embora eu não lide com um cancelamento em meu código, queria ter a certeza de que você está ciente desse padrão e, portanto, incluí o token assim mesmo.

Com todas as partes criadas, o URI, o httpContent e a CancellationTokenSource, posso finalmente fazer a chamada para o meu Azure Function usando o método HttpClient.PostAsync. A resposta volta como JSON. Meu código a lê e usa o método JsonConvert de JSON.Net para desserializar a resposta em qualquer objeto que tenha sido especificado pelo método de chamada.

Se você examinar o código novamente em busca de GetTopGlobalScores, verá que especifiquei que os resultados devem ser um List<ScoreViewModel>. ScoreViewModel é um tipo que criei para corresponder ao esquema dos dados de pontuação retornados por dois dos Azure Functions. A classe também tem algumas propriedades adicionais que formatam os dados com base em como eu quero exibi-los no aplicativo UWP. Já que a classe Score­ViewModel é uma listagem longa, permitirei que você inspecione o código na amostra do download.

Como chamar um Azure Function que obtém um parâmetro

Ainda há outros dois Azure Functions a explorar. Vamos examinar o outro que retorna os dados de pontuação. Essa função precisa da UserId passada, enquanto a função anterior não recebia entrada. Mas, nesse caso, ainda não é necessário criar o HttpJsonContent porque, se você se lembra da função como descrita no artigo do mês passado, ela espera o valor de UserId a ser passado como parte do URI. O exemplo simples do artigo do mês passado usa a cadeia de caracteres 54321 como a UserId neste URI: https://cookiebinge.azurewebsites.net/­api/GetUserScores/54321. Com a adição do recurso de gerenciamento de identidades ao aplicativo, a UserId agora será um GUID.

Não me aprofundarei no código sobre como a identidade de um usuário é gerenciada, mas vamos dar uma rápida olhada. Você será capaz de ver todo esse código no download. Criei um novo par de Azure Functions para o gerenciamento de usuários. Quando um usuário opta por se registrar na nuvem para o acompanhamento da pontuação, uma dessas funções cria um novo GUID para a UserId, o armazena em uma coleção separada no banco de dados do Cosmos DB do CookieBinge e retorna o GUID para o aplicativo UWP. O aplicativo UWP então usa o EF Core 2 para armazenar a UserId em uma nova tabela no banco de dados local. Parte do GUID é exibido para os usuários na página da conta deles, como mostrado na Figura 1. Quando um usuário joga o CookieBinge em outro dispositivo, adquire o GUID completo ao enviar o GUID parcial disponível em qualquer outro dispositivo já registrado para outro Azure Function. Essa função retorna o GUID completo e o aplicativo e então armazena a UserId no dispositivo atual. Dessa maneira, o usuário pode postar pontuações de qualquer um dos dispositivos na nuvem, sempre usando a mesma UserId. Adicionalmente, o aplicativo pode usar a mesma UserId para recuperar as pontuações de todos os dispositivos da nuvem. Uma classe AccountService.cs tem funcionalidade para as interações locais relacionadas à UserId, incluindo o armazenamento e a recuperação da UserId desde o banco de dados local. Eu resolvi usar esse padrão por minha conta e me dei os parabéns por ser tão inteligente, mesmo quando poderia ter aproveitado uma estrutura existente.

GetUserTopScores é o método em CloudServices que chama a função GetUserScores. Como o método anterior, ele chama o método CallCookieBingeFunctionAsync, novamente esperando que o tipo retornado seja uma lista de objetos ScoreViewModel. Novamente, estou passando somente um único parâmetro, que não é só o nome da função, mas a cadeia de caracteres completa que deve ser anexada à URL base. Estou usando a interpolação de cadeia de caracteres para combinar o nome da função com os resultados da propriedade AccountService.AccountId:

public async Task<List<ScoreViewModel>> GetUserTopScores()
{
  var results = await CallCookie­Binge­Function­Async<List<ScoreViewModel>>
    ($"GetUserScores\\­{AccountService.AccountId}");
  return results;
}

Como chamar um Azure Function que espera conteúdo JSON na solicitação

O Azure Function final, StoreScores, dá a oportunidade de mostrar como anexar o JSON a uma HttpRequest. StoreScores obtém um objeto JSON e armazena seus dados no banco de dados do Cosmos DB. A Figura 2 serve como um lembrete de como testei a função no Portal do Azure ao enviar um objeto JSON que segue o esquema esperado.

A exibição do Portal do Azure da função StoreScores testada com um corpo de solicitação JSON
Figura 2 A exibição do Portal do Azure da função StoreScores testada com um corpo de solicitação JSON

Para fazer a correspondência do esquema no aplicativo UWP, criei um struct DTO (objeto de transferência de dados) chamado StoreScoreDto, o que me ajuda a criar o corpo JSON para a solicitação. Este é o método CloudService.SendBingeToCloudAsync, que obtém os dados de Binge resultantes do jogo e os envia o Azure Function com o auxílio do mesmo método CallCookieBingeFunctionAsync usado para chamar as duas outras funções:

public async void SendBingeToCloudAsync(int count, bool worthIt,
  DateTime timeOccurred)
{
  var storeScore = new StoreScoreDto(AccountService.AccountId,
                                     "Julie", AccountService.DeviceName,
                                     timeOccurred, count, worthIt);
  var jsonScore = JsonConvert.SerializeObject(storeScore);
  var jsonValueScore = JsonValue.Parse(jsonScore);
  var results = await CallCookieBingeFunctionAsync<string>("StoreScores",
    jsonValueScore);
}

SendBingeToCloudAsync começa ao obter os dados relevantes sobre o Binge a ser armazenado — a contagem de cookies consumidos, tendo a comilança valido a pena ou não. Então crio um objeto StoreScoreDto desde os dados e uso JsonConvert novamente, dessa vez para serializar o StoreScoreDto em um objeto JSON. A próxima etapa é criar um JsonValue, como expliquei anteriormente, um tipo especial no namespace Windows.Json.Data. Faço isso usando o método JsonValue.Parse, passando o objeto JSON representado por jsonScore. O JsonValue resultante é o formato necessário para o envio do objeto JSON junto com a solicitação HTTP. Agora que tenho um JsonValue adequadamente formatado, posso enviá-lo para o método CallCookeBingeFunctionAsync junto com o nome da função, StoreScores. Observe que o tipo que espero que seja retornado seja uma cadeia de caracteres, que será uma notificação do Azure Function StoreScores do êxito ou da falha da função.

Como conectar a interface do usuário ao CloudService

Com os métodos CloudService prontos, finalmente posso garantir que a interface do usuário interaja com eles. Lembre-se de que quando um Binge é salvo, o código em MainPage.xaml.cs chama um método em BingeService que armazena os dados no banco de dados local. Esse mesmo método, mostrado na Figura 3, agora também envia os dados da comilança para o CloudService para armazená-los na nuvem por meio do Azure Function StoreScores.

Figura 3 O método RecordBinge pré-existente agora envia o Binge para a nuvem

public static  void RecordBinge(int count, bool worthIt)
{
  var binge = new CookieBinge{HowMany = count, WorthIt = worthIt,
                              TimeOccurred = DateTime.Now};
  using (var context = new BingeContext(options))
  {
    context.Binges.Add(binge);
    context.SaveChanges();
  }
  using (var cloudService = new BingeCloudService())
  {
    cloudService.SendBingeToCloudAsync(count, worthIt, binge.TimeOccurred);
  }
}

Os outros dois métodos que interagem com os Azure Functions retornam listas de objetos ScoreViewModel.

Para exibir as pontuações armazenadas na nuvem, como mostrado na Figure 1, adicionei um método a MainWindow.xaml.cs, que chama os métodos CloudService para recuperar as pontuações e então as vincular às ListViews relevantes na página. Chamei esse método de ReloadScores porque ele também é chamado pelo botão de atualização na mesma página:

private async Task ReloadScores()
{
  using (var cloudService = new BingeCloudService())
  {
    YourScoresList.ItemsSource = await cloudService.GetUserTopScores();
    GlobalScoresList.ItemsSource =
      await cloudService.GetTopGlobalScores();
  }
}

Então, a interface do usuário exibe os dados de pontuação com base nos modelos definidos para cada lista na página. Por exemplo, a Figura 4 mostra XAML para exibição das GlobalScores na interface do usuário.

Figura 4 Dados XAML vinculando os dados de pontuação retornados de um Azure Function

<ListView  x:Name="GlobalScoresList"    >
  <ListView.ItemTemplate>
    <DataTemplate >
      <StackPanel Orientation="Horizontal">
        <TextBlock FontSize="24" Text="{Binding score}"
                   VerticalAlignment="Center"/>
        <TextBlock FontSize="16" Text="{Binding displayGlobalScore}"
                   VerticalAlignment="Center" />
      </StackPanel>
    </DataTemplate>
  </ListView.ItemTemplate></ListView>

Como agrupar esta série de quatro partes

O que começou como um exercício para experimentar a última versão do EF Core 2 em dispositivos móveis baseados no Windows, transformou-se em um aventura para mim, e espero que tenha sido uma jornada divertida, interessante e educacional para você também. Trabalhar com o novo UWP baseado no .NET Standard 2.0, especialmente no início destes dias de pré-lançamento, foi com certeza desafiador para esta desenvolvedora de back-end. Mas eu amei a ideia de ser capaz de armazenar dados localmente e na nuvem, além de obter novas habilidades pelo caminho.

A segunda e a terceira colunas da série foram minhas primeiras experiências com Azure Functions e estou muito feliz de ter tido uma desculpa para fazer isso, já que agora sou uma grande fã dessa tecnologia e fiz muitas outras coisas com ela desde essas quatro primeiras etapas. Eu certamente espero que você também tenha se inspirado!

Como vimos neste artigo, a interação com estas funções desde o aplicativo UWP não é tão simples como minha última experiência de fazer chamadas Web de outras plataformas. Pessoalmente, fiquei muito satisfeita ao descobrir como era o fluxo de trabalho.

Se você conferir o download, verá a outra adição feita ao aplicativo — toda a lógica para o registro no armazenamento na nuvem, salvando a UserId gerada pelo Azure, bem como um nome para o dispositivo, e o registro de dispositivos adicionais e então o acesso da UserId e do nome do dispositivo para uso nos métodos StoreScores e GetUserScores. Baixei o aplicativo Azure Function inteiro para um projeto .NET para que você pudesse ver e interagir com todas as funções que dão suporte ao aplicativo. Passei um tempo surpreendente examinando o fluxo de trabalho de identidade e, de certa forma, tornei-me obcecada com a diversão dessa descoberta. Talvez eu também escreva sobre isso algum dia.


Julie Lerman é Diretora Regional da Microsoft, MVP da Microsoft, coach e consultora de equipes de software e reside nas colinas de Vermont. Você pode encontrá-la em apresentações sobre acesso de dados ou sobre outros tópicos em grupos de usuários e conferências em todo o mundo. Ela escreve no blog thedatafarm.com/blog e é autora do “Programming Entity Framework”, bem como de uma edição do Code First e do DbContext, todos da O'Reilly Media. Siga-a no Twitter em @julielerman e confira seus cursos da Pluralsight em juliel.me/PS-Videos.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Ginny Caughey (Carolina Software Inc.)
Ginny Caughey é presidente da Carolina Software, Inc., que fornece software e serviços para o setor de resíduos sólidos nos EUA e no Canadá. Em seu tempo livre, ela também criou o Password Padlock para Windows e Windows Phone. Ela é participante ativa do Twitter (@gcaughey) e MVP de Desenvolvimento do Windows.


Discuta esse artigo no fórum do MSDN Magazine