2018 年 3 月

第 33 卷,第 3 期

数据点 - 通过通用 Windows 平台调用 Azure 函数

作者 Julie Lerman

阅读整个 EF Core 2 和 UWP 应用数据系列:

使用 EF Core 2 和 Azure Functions 本地和全局存储 UWP 应用数据 - 第 1 部分
使用 EF Core 2 和 Azure Functions 本地和全局存储 UWP 应用数据 - 第 2 部分
使用 EF Core 2 和 Azure Functions 本地和全局存储 UWP 应用数据 - 第 3 部分
使用 EF Core 2 和 Azure Functions 本地和全局存储 UWP 应用数据 - 第 4 部分

Julie Lerman本系列专栏是关于生成在本地和云中存储数据的通用 Windows 平台 (UWP) 应用,本文是最后一期。在第一期中,我生成了 UWP Cookie­Binge 游戏,它使用 Entity Framework Core 2 (EF Core 2) 将游戏得分存储到玩此游戏所用的设备上。在后两期中,我介绍了如何在云中生成 Azure 函数,以将游戏得分存储到 Microsoft Azure Cosmos DB 数据库和从中检索得分。在本系列专栏的最后一篇文章中,我将介绍如何在 UWP 应用中向 Azure 函数发出请求,以发送游戏得分,并接收和显示在她全部设备上的最高几个得分,以及世界各地所有玩家的最高几个得分。

借助此解决方案,用户还可以在云中注册,并将自己的任何一台设备与注册绑定。尽管我将利用此注册信息通过 Azure 函数发送和检索得分,但我不会介绍这部分的应用。不过,下载内容中包含相应代码,包括我为注册而新建的 Azure 函数的相关代码。

接着上期继续说下去

稍微回顾一下有助于重新认识 UWP 应用和我将连接的 Azure 函数。

在 CookieBinge 游戏中,当用户完成暴食后,有两个按钮可供选择,以告知应用已大功告成。一个是“值得”按钮,表示用户已大功告成,且对狼吞虎咽地吃下去的所有饼干感到满意。另一个则是“不值得”按钮。为了响应此类单击事件,逻辑指向 BingeServices.RecordBinge 方法,以使用 EF Core 2 将数据存储到本地数据库。

此应用还有一项功能,即显示本地数据库中的前五位游戏得分。

我在本系列专栏的上几期文章中生成了三个 Azure 函数。第一个函数用于接收游戏得分和玩家 UserId(由注册功能分配),以及玩游戏所用设备的名称。然后,这个函数会将此类数据存储到 Cosmos DB 数据库。另外两个函数用于响应从 Cosmos DB 数据库获取数据的请求。一个用于接收玩家 UserId,并返回玩游戏所用全部设备发送的前五位得分。另一个仅返回数据库中存储的前五位得分,而不考虑玩家是谁。

所以,当前任务是将 Azure 函数集成到游戏中。首先,将游戏得分存储到本地数据库时,此应用还应将得分和其他相关数据发送到 StoreScore 函数。其次,在应用从本地数据库读取得分历史记录时,它还应向函数发送请求,以返回得分并显示此类请求的结果,如图 1 所示。

通过 Azure 函数从 Azure Cosmos DB 数据库检索到的得分
图 1:通过 Azure 函数从 Azure Cosmos DB 数据库检索到的得分

通过 UWP 与 Web 通信

UWP 框架使用一组特殊的 API 来发出 Web 请求和接收响应。实际上,有两个命名空间可供选择。我建议阅读以下 MSDN 博客文章:“阐明通用 Windows 平台中的 HttpClient API”(网址为 bit.ly/2rxZu3f)。我将使用 Windows.Web.Http 命名空间提供的 API。对于如何将数据与任何请求一起发送,这些 API 有非常明确的要求。也就是说,需要额外完成一些工作。如果我需要将某 JSON 与我的请求一起发送,我利用的是帮助程序类 HttpJsonContent。它会合并 JSON 内容和头内容,并额外执行某逻辑。HttpJsonContent 要求以 Windows.Data.Json.JsonValue 格式发送 JSON。因此,大家将会看到我是在怎样的情况下执行了这些步骤。此外,我可以显式设置 HttpRequest 的头内容,再使用 StringContent 方法将数据发布为字符串,如 bit.ly/2BBjFNE 中所示。

我将用于与 Azure 函数进行交互的所有逻辑都封装到了 CloudService.cs 类中。

在确定 UWP 请求方法的使用模式和如何创建 JsonValue 对象后,我便创建了 CallCookieBingeFunctionAsync 方法,用于封装此逻辑的大部分。此方法需要使用以下两个参数:要调用的 Azure 函数名称和 JsonValue 对象。我还创建了此方法的重载,无需使用 JsonValue 对象参数。

下面展示了此方法的签名:

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

由于我需要调用三个不同的 Azure 函数,因此将从最简单的函数 GetTop5GlobalUserScores 入手。此函数无需使用任何参数或其他内容,并以 JSON 形式返回结果。

CloudService 类中的 GetTopGlobalScores 方法调用我新建的 CallCookieBingeFunctionAsync 方法,同时传递函数的名称,再返回函数响应中的结果。

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

请注意,我不会向此方法传递另一个参数。也就是说,我创建的重载(无需使用 JsonValue)将获调用:

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

这进而会调用此方法的另一版本,并在需要 JsonValue 的情况下仅传递 NULL。下面展示了 CallCookieBingeFunctionAsync 方法的完整列表(确实需要解释):

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

在第一步中,此方法创建 Windows.Web.Http.HttpClient 实例。然后,此方法构造通过 HttpClient 发出请求所需的信息,首先就是要调用的函数的 Web 地址。我的所有函数都是以 https://cookiebinge.azurewebsites.net/­api/ 开头,因此我已将相应值硬编码到此方法中,再将传递的函数名称追加到此方法中。

接下来,我必须定义头和传递给此函数的任何内容。正如我刚才所解释,我已选择使用帮助程序类 HttpJsonContent 执行这一步。我是从官方 Windows 通用示例的 JSON 部分 (bit.ly/2ry7mBP) 复制了此类,让我能够将 JsonValue 对象转换成实现 IHttpContent 的对象。(下载内容中包含我复制的完整类。) 如果未将 JsonValue 传递给此方法(就像对 GetTop5GlobalUserScores 函数那样),httpContent 变量为 NULL。

此方法的下一步是在变量 cts 中定义 CancellationTokenSource。尽管我不会在代码中处理取消操作,但为了确保大家能够注意到这种模式,我还是添加了此令牌。

所有部分(URI、httpContent 和 CancellationTokenSource)现已构造完成,我终于可以使用 HttpClient.PostAsync 方法调用我的 Azure 函数了。响应以 JSON 形式返回。我的代码会读取响应,并使用 JSON.Net 的 JsonConvert 方法,将响应反序列化为调用方法指定的任何对象。

如果回顾一下 GetTopGlobalScores 代码,就会发现我已指定结果应为 List<ScoreViewModel>。ScoreViewModel 是我创建的类型,用于匹配两个 Azure 函数返回的得分数据的架构。此类还有其他一些属性,可根据我希望如何在 UWP 应用中显示数据来设置数据格式。鉴于 Score­ViewModel 类是长列表,我建议在下载示例中查看它的代码。

调用需要使用参数的 Azure 函数

仍有两个 Azure 函数有待探索。接下来看一看另一个返回得分数据的函数。此函数需要有 UserId 传入,而上一个函数则无需任何输入数据。不过,在此示例中,我仍不需要生成 HttpJsonContent,因为如果回顾一下本系列专栏上月文章中介绍的函数,就会发现 UserId 值应作为 URI 的一部分进行传递。上月那篇文章中的简单示例就是将字符串 54321 用作此 URI 中的 UserId:https://cookiebinge.azurewebsites.net/­api/GetUserScores/54321。通过向应用添加标识管理功能,UserId 现成为 GUID。

我不会深入介绍关于如何管理用户标识的代码,但还是会快速介绍一下。下载内容中包含此代码的全部详情。我新建了一对 Azure 函数,以用于用户管理。如果用户选择注册云以跟踪得分,这两个函数之一就会新建 UserId 的 GUID,将它存储在 CookieBinge Cosmos DB 数据库内的单独集合中,再将 GUID 返回到 UWP 应用。然后,UWP 应用使用 EF Core 2 将相应 UserId 存储到本地数据库内的新表中。部分 GUID 在用户帐户页面(如图 1 所示)上显示给用户。如果用户在其他设备上玩 CookieBinge 游戏,可以发送已注册到另一个 Azure 函数的其他任何设备上的部分 GUID,从而获取完整 GUID。此函数会返回完整 GUID,然后应用会将相应 UserId 存储到当前设备上。这样一来,用户可以始终使用同一 UserId,将自己所有设备中的得分发布到云中。此外,应用还可以使用同一 UserId 从云中检索所有设备内的得分。AccountService.cs 类可以执行与 UserId 相关的本地交互,包括存储和检索本地数据库中的 UserId。我独自提出了这种模式,给自己鼓鼓劲,感觉自己太聪明了,尽管我本可以利用现有框架。

GetUserTopScores 是 CloudServices 中调用 GetUserScores 函数的方法。与上一方法类似,它调用 CallCookieBingeFunctionAsync 方法,返回类型也应为 ScoreViewModel 对象列表。我会再次只传递一个参数,它不仅是函数的名称,还是应追加到基 URL 的完整字符串。我将使用字符串内插,将函数名称和 AccountService.AccountId 属性结果合并到一起:

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

调用要求将 JSON 内容与请求一起发送的 Azure 函数

最后一个 Azure 函数 StoreScores 让我有机会介绍如何将 JSON 追加到 HttpRequest。StoreScores 获取 JSON 对象,并将它的数据存储到 Cosmos DB 数据库中。图 2 展示了我是如何通过发送采用要求架构的 JSON 对象,在 Azure 门户中测试此函数的。

使用 JSON 请求正文测试 StoreScores 函数的 Azure 门户视图
图 2:使用 JSON 请求正文测试 StoreScores 函数的 Azure 门户视图

为了匹配 UWP 应用中的架构,我创建了数据传输对象 (DTO) StoreScoreDto,有助于创建请求的 JSON 正文。下面展示了 CloudService.SendBingeToCloudAsync 方法,它需要使用用户玩游戏生成的 Binge 数据,并借助我用来调用其他两个函数的同一 CallCookieBingeFunctionAsync 方法,将数据发送到 Azure 函数:

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 首先获取要存储的 Binge 相关数据:吃下去的饼干数量、暴食是否值得以及具体发生时间。然后,我根据此类数据创建 StoreScoreDto 对象,并再次使用 JsonConvert,这一次是将 StoreScoreDto 序列化为 JSON 对象。下一步是创建 JsonValue。如前所述,它是 Windows.Json.Data 命名空间中的特殊类型。为此,我使用 JsonValue.Parse 方法,同时传递 jsonScore 表示的 JSON 对象。生成的 JsonValue 是将 JSON 对象和 HTTP 请求一起发送所必需的格式。至此,我已正确格式化 JsonValue,可以将它与 StoreScores 函数的名称一起发送到 CallCookeBingeFunctionAsync 方法。请注意,返回类型应为字符串,这将是 StoreScores Azure 函数通知,指明函数成功还是失败。

将 UI 连接到 CloudService

在 CloudService 方法就位后,我终于可以确保 UI 能够与它们进行交互了。回顾一下,保存 Binge 时,MainPage.xaml.cs 中的代码会调用 BingeService 中的方法,以将相应数据保存到本地数据库。现在,图 3 中的同一方法也会将 Binge 数据发送到 CloudService,以通过 StoreScores Azure 函数将它存储到云中。

图 3:现有 RecordBinge 方法现在向云发送 Binge 数据

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

与 Azure 函数交互的另外两种方法均返回 ScoreViewModel 对象列表。

为了显示在云中存储的得分(如图 1**** 所示),我向 MainWindow.xaml.cs 添加了一个方法,以调用 CloudService 方法来检索得分,再将它们绑定到页面上的相关 ListView。我将此方法命名为 ReloadScores,因为它也由同一页面上的刷新按钮调用:

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

然后,此 UI 根据为页面上的每个列表定义的模板显示得分数据。例如,图 4 展示了用于在 UI 中显示 GlobalScores 的 XAML。

图 4:从 Azure 函数返回的得分数据的 XAML 数据绑定

<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>

本系列专栏(四部分)总结

一开始我是在基于 Windows 的移动设备上试用最新版 EF Core 2,之后这就带我体验了一场相当精彩的冒险之旅。我希望这对大家来说也是一场快乐、有趣且有教育意义的旅程。对于后端开发人员来说,使用全新基于 .NET Standard 2.0 的 UWP 无疑是一项挑战,特别是在预发行早期阶段。不过,我热衷的是,能够在本地和云中存储数据,同时还能掌握新技能。

本系列专栏的第二篇和第三篇文章是我初次体验使用 Azure Functions,我很高兴有理由这么做,因为我现在对这项技术十分推崇,并且自初次尝试后完成了更多的工作。我当然希望大家也能受到同样的启发!

正如本文所述,通过 UWP 应用与这些函数交互,并不像从前通过其他平台调用 Web 那样简单。我个人极为满意的是确定了工作流。

如果查看下载内容,就会发现我向此应用添加了其他内容,所有这些逻辑都是用于注册云存储,保存 Azure 生成的 UserId 和设备名称,注册其他设备,再访问用于 StoreScores 和 Get­UserScores 方法的 UserId 和设备名称。我已将整个 Azure Function App 下载到 .NET 项目中,以便大家能够查看支持应用的所有函数,并与之交互。我在研究标识工作流上花费的时间惊人之多,并有点痴迷于解决问题带来的快感。或许某天我也会撰写这方面的文章。


Julie Lerman**** 住在佛蒙特州的丘陵地区,担任 Microsoft 区域主管、Microsoft MVP、软件团队导师和顾问。可以在全球的用户组和会议中看到她对数据访问和其他主题的介绍。她的博客地址是 thedatafarm.com/blog。她是“Entity Framework 编程”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。通过 Twitter 关注她:@julielerman 并在 juliel.me/PS-Videos 上观看其 Pluralsight 课程。

衷心感谢以下技术专家对本文的审阅:Ginny Caughey (Carolina Software Inc.)
Ginny Caughey 是 Carolina Software, Inc. 总裁。此公司为美国和加拿大的固体废弃物行业提供软件和服务。业余时间里,她还是“Windows 和 Windows Phone 密码挂锁”的作者。她活跃于 Twitter (@gcaughey),并且还是一位 Windows 开发 MVP。


在 MSDN 杂志论坛讨论这篇文章