教程:使用 Azure 通知中心通过后端服务向 Xamarin.Forms 应用发送推送通知Tutorial: Send push notifications to Xamarin.Forms apps using Azure Notification Hubs via a backend service

下载示例下载示例Download Sample Download the sample

在本教程中,将使用 Azure 通知中心将通知推送到适用于 iOS 的 Xamarin.Forms 应用程序。In this tutorial, you use Azure Notification Hubs to push notifications to a Xamarin.Forms application targeting iOS.

使用 ASP.NET Core Web API 后端,通过最新且最理想的安装方法来处理客户端的设备注册An ASP.NET Core Web API backend is used to handle device registration for the client using the latest and best Installation approach. 服务还将以跨平台的方式发送推送通知。The service will also send push notifications in a cross-platform manner.

借助用于后端操作的通知中心 SDK 处理这些操作。These operations are handled using the Notification Hubs SDK for backend operations. 从应用后端注册文档中提供了整体方法的更多详细信息。Further detail on the overall approach is provided in the Registering from your app backend documentation.

本教程将指导你完成以下步骤:This tutorial takes you through the following steps:

先决条件Prerequisites

若要继续操作,需要:To follow along, you require:

对于 iOS 而言,必须具有:For iOS, you must have:

备注

IOS 模拟器不支持远程通知,因此在 iOS 上浏览此示例时需要使用物理设备。The iOS Simulator does not support remote notifications and so a physical device is required when exploring this sample on iOS.

可以遵循此首要原则示例中的步骤,而不需要事先体验。You can follow the steps in this first-principles example with no prior experience. 不过,如能熟悉以下方面则会更有帮助。However, you'll benefit from having familiarity with the following aspects.

提供的步骤适用于 Visual Studio for Mac但使用 Visual Studio 2019 的用户也可按此操作。The steps provided are for Visual Studio for Mac but it's possible to follow along using Visual Studio 2019.

设置推送通知服务和 Azure 通知中心Set up Push Notification Services and Azure Notification Hub

在此部分中,将设置 Apple Push Notification 服务 (APNS)In this section, you set up Apple Push Notification Services (APNS). 然后,创建并配置通知中心来处理这些服务。You then create and configure a notification hub to work with those services.

为推送通知注册 iOS 应用Register your iOS app for push notifications

若要将推送通知发送到 iOS 应用,请向 Apple 注册应用程序,还要注册推送通知。To send push notifications to an iOS app, register your application with Apple, and also register for push notifications.

  1. 如果尚未注册应用,请浏览到 Apple 开发人员中心的 iOS 预配门户If you haven't already registered your app, browse to the iOS Provisioning Portal at the Apple Developer Center. 使用 Apple ID 登录门户,导航到“证书、标识符和配置文件”,然后选择“标识符” 。Sign in to the portal with your Apple ID, navigate to Certificates, Identifiers & Profiles, then select Identifiers. 单击“+”注册新应用。Click + to register a new app.

    iOS 预配门户应用 ID 页

  2. 在“注册新的标识符”屏幕上,选择“应用 ID”单选按钮。On the Register a New Identifier screen, select the App IDs radio button. 然后选择“继续”。Then select Continue.

    iOS 预配门户“注册新 ID”页

  3. 更新新应用的以下三个值,然后选择“Continue”(继续):Update the following three values for your new app, and then select Continue:

    • 说明:键入应用的描述性名称。Description: Type a descriptive name for your app.

    • 捆绑 ID:按应用分发指南中所述,输入格式为 com.<organization_identifier>.<product_name> 的捆绑 ID。Bundle ID: Enter a Bundle ID of the form com.<organization_identifier>.<product_name> as mentioned in the App Distribution Guide. 在下面的屏幕截图中,mobcat 值用作组织标识符,PushDemo 值用作产品名称。In the following screenshot, the mobcat value is used as an organization identifier and the PushDemo value is used as the product name.

      iOS 预配门户“注册应用 ID”页

    • 推送通知:在“Capabilities”(功能)部分选中“Push Notifications”(推送通知)选项 。Push Notifications: Check the Push Notifications option in the Capabilities section.

      用于注册新应用 ID 的窗体

      此操作会生成应用 ID 并请求你确认信息。This action generates your App ID and requests that you confirm the information. 选择“Continue”(继续),然后选择“Register”(注册) 以确认新的应用 ID。Select Continue, then select Register to confirm the new App ID.

      确认新的应用 ID

      选择“Register”(注册)后,新的应用 ID 将作为行项出现在“Certificates, Identifiers & Profiles”(证书、标识符和配置文件)页中。After you select Register, you see the new App ID as a line item in the Certificates, Identifiers & Profiles page.

  4. 在“证书、标识符和配置文件”页上的“标识符”下,找到创建的应用 ID 行项 。In the Certificates, Identifiers & Profiles page, under Identifiers, locate the App ID line item that you created. 然后,选择其行以显示“编辑应用 ID 配置”屏幕。Then, select its row to display the Edit your App ID Configuration screen.

为通知中心创建证书Creating a certificate for Notification Hubs

若要使通知中心能够使用 Apple Push Notification 服务 (APNS),需要使用证书,可以通过以下两种方式之一提供证书:A certificate is required to enable the notification hub to work with Apple Push Notification Services (APNS) and can be provided in one of two ways:

  1. 创建可以直接上传到通知中心的 p12 推送证书(原始方法)Creating a p12 push certificate that can be uploaded directly to Notification Hub (the original approach)

  2. 创建可用于基于令牌的身份验证的 p8 证书(推荐的较新方法)Creating a p8 certificate that can be used for token-based authentication (the newer and recommended approach)

基于令牌 (HTTP/2) 的 APNS 身份验证中所述,较新的方法有很多好处。The newer approach has a number of benefits as documented in Token-based (HTTP/2) authentication for APNS. 所需的步骤较少,但一些步骤对于特定的场景来说也是必需的。Fewer steps are required but is also mandated for specific scenarios. 不过,这两种方法的步骤均已提供,因为在本教程中,这两种方法均适用。However, steps have been provided for both approaches since either will work for the purposes of this tutorial.

选项 1:创建可以直接上传到通知中心的 p12 推送证书OPTION 1: Creating a p12 push certificate that can be uploaded directly to Notification Hub
  1. 在 Mac 上,运行 Keychain Access 工具。On your Mac, run the Keychain Access tool. 可以从启动台上的“Utilities”或“Other”文件夹中打开该工具。 It can be opened from the Utilities folder or the Other folder on the Launchpad.

  2. 选择“Keychain Access”,展开“Certificate Assistant”(证书助理),然后选择“Request a Certificate from a Certificate Authority”(从证书颁发机构请求证书)。 Select Keychain Access, expand Certificate Assistant, and then select Request a Certificate from a Certificate Authority.

    使用 Keychain Access 请求新证书

    备注

    默认情况下,Keychain Access 选择列表中的第一项。By default, Keychain Access selects the first item in the list. 如果你位于“Certificates”(证书)类别中,并且“Apple Worldwide Developer Relations Certification Authority”(Apple 全球开发者关系证书颁发机构)不是列表中的第一项,这可能会是个问题。This can be a problem if you're in the Certificates category and Apple Worldwide Developer Relations Certification Authority is not the first item in the list. 在生成 CSR(证书签名请求)之前,请确保已有非密钥项,或者已选择“Apple Worldwide Developer Relations Certification Authority”(Apple 全球开发者关系证书颁发机构)密钥。Make sure you have a non-key item, or the Apple Worldwide Developer Relations Certification Authority key is selected, before generating the CSR (Certificate Signing Request).

  3. 选择“User Email Address”(用户电子邮件地址),输入“Common Name”(公用名)值,确保指定“Saved to disk”(保存到磁盘),然后选择“Continue”(继续)。 Select your User Email Address, enter your Common Name value, make sure that you specify Saved to disk, and then select Continue. 将“CA Email Address”(CA 电子邮件地址)留空,因为它不是必填字段。Leave CA Email Address blank as it isn't required.

    预期的证书信息

  4. 在“另存为”中为证书签名请求 (CSR) 文件输入一个名称,在“位置”中选择一个位置,并选择“保存” 。Enter a name for the Certificate Signing Request (CSR) file in Save As, select the location in Where, and then select Save.

    为证书选择一个文件名

    此操作会将 CSR 文件保存到选定位置。This action saves the CSR file in the selected location. 默认位置为“桌面”。The default location is Desktop. 请记住为此文件选择的位置。Remember the location chosen for the file.

  5. 返回到 iOS 预配门户的“证书、标识符和配置文件”页,向下滚动到已选择的“推送通知”选项,然后选择“配置”来创建证书 。Back on the Certificates, Identifiers & Profiles page in the iOS Provisioning Portal, scroll down to the checked Push Notifications option, and then select Configure to create the certificate.

    编辑应用 ID 页

  6. 此时将显示“Apple Push Notification 服务 TLS/SSL 证书”窗口。The Apple Push Notification service TLS/SSL Certificates window appears. 选择“开发 TLS/SSL 证书”部分下的“创建证书”按钮 。Select the Create Certificate button under the Development TLS/SSL Certificate section.

    “为应用 ID 创建证书”按钮

    此时将显示“Create a new Certificate”(创建新证书)屏幕。The Create a new Certificate screen is displayed.

    备注

    本教程使用开发证书。This tutorial uses a development certificate. 注册生产证书时使用相同的过程。The same process is used when registering a production certificate. 只需确保在发送通知时使用相同的证书类型。Just make sure that you use the same certificate type when sending notifications.

  7. 选择“选择文件”,浏览到保存 CSR 文件的位置,然后双击证书名以加载该证书 。Select Choose File, browse to the location where you saved the CSR file, and then double-click the certificate name to load it. 然后选择“继续”。Then select Continue.

  8. 当门户创建证书后,请选择“Download”(下载)按钮。After the portal creates the certificate, select the Download button. 保存证书,并记住保存证书的位置。Save the certificate, and remember the location to which it's saved.

    已生成证书的下载页

    这将下载证书并将其保存到计算机的 Downloads 文件夹。The certificate is downloaded and saved to your computer in your Downloads folder.

    在 Downloads 文件夹中找到证书文件

    备注

    默认情况下,下载的开发证书名为 aps_development.cerBy default, the downloaded development certificate is named aps_development.cer.

  9. 双击下载的推送证书 aps_development.cerDouble-click the downloaded push certificate aps_development.cer. 此操作将在密钥链中安装新证书,如下图所示:This action installs the new certificate in the Keychain, as shown in the following image:

    Keychain Access 证书列表,显示了新证书

    备注

    证书中的名称可能有所不同,但会以 Apple Development iOS Push Services 作为前缀,并且关联有相应捆绑标识符。Although the name in your certificate might be different, the name will be prefixed with Apple Development iOS Push Services and have the appropriate bundle identifier associated with it.

  10. 在 Keychain Access 中,按住 Control 的同时单击在“证书”类别中创建的新推送证书 。In Keychain Access, Control + Click on the new push certificate that you created in the Certificates category. 选择“导出”,为文件命名,选择“p12”格式,并选择“保存”。 Select Export, name the file, select the p12 format, and then select Save.

    将证书作为 p12 格式导出

    可以选择使用密码保护证书,但这是可选的。You can choose to protect the certificate with a password, but a password is optional. 如果要跳过密码创建,请单击“OK”(确定)。Click OK if you want to bypass password creation. 记下导出的 p12 证书的文件名和位置。Make a note of the file name and location of the exported p12 certificate. 它们用于启用 APNS 身份验证。They're used to enable authentication with APNs.

    备注

    你的 p12 文件名和位置可能不同于本教程中所示的名称和位置。Your p12 file name and location might be different than what is pictured in this tutorial.

选项 2:创建可用于基于令牌的身份验证的 p8 证书OPTION 2: Creating a p8 certificate that can be used for token-based authentication
  1. 请记下以下详细信息:Make note of the following details:

    • 应用 ID 前缀(团队 ID )App ID Prefix (Team ID)
    • 捆绑包 IDBundle ID
  2. 返回到“证书、标识符和配置文件”,单击“密钥”。Back in Certificates, Identifiers & Profiles, click Keys.

    备注

    如果已为 APNS 配置了密钥,则可以重复使用在创建后立即下载的 p8 证书。If you already have a key configured for APNS, you can re-use the p8 certificate that you downloaded right after it was created. 如果是这样,则可以忽略步骤 3 到步骤 5。If so, you can ignore steps 3 through 5.

  3. 单击 + 按钮(或“创建密钥”按钮)以创建新密钥。Click the + button (or the Create a key button) to create a new key.

  4. 提供合适的“密钥名称”值,选中“Apple Push Notification 服务(APNS)”选项,然后单击“继续”,接下来在下一个屏幕上单击“注册”。Provide a suitable Key Name value, then check the Apple Push Notifications service (APNS) option, and then click Continue, followed by Register on the next screen.

  5. 单击“下载”,然后将 p8 文件(前缀为 AuthKey_)移动到安全的本地目录,然后单击“完成”。Click Download and then move the p8 file (prefixed with AuthKey_) to a secure local directory, then click Done.

    备注

    请确保将 p8 文件保存在安全的位置(并保存备份)。Be sure to keep your p8 file in a secure place (and save a backup). 密钥在下载后无法重新下载,因为服务器副本已删除。After downloading your key, it cannot be re-downloaded as the server copy is removed.

  6. 在“密钥”上,单击创建的密钥(如果已选择使用现有密钥,则改为单击现有密钥)。On Keys, click on the key that you created (or an existing key if you have chosen to use that instead).

  7. 记下“密钥 ID”值。Make note of the Key ID value.

  8. 在所选的合适应用程序(如 Visual Studio Code)中打开 p8 证书。Open your p8 certificate in a suitable application of your choice such as Visual Studio Code. 记下密钥值(位于-----BEGIN PRIVATE KEY----- 和 -----END PRIVATE KEY----- 之间) 。Make note of the key value (between -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----).

    -----BEGIN PRIVATE KEY----------BEGIN PRIVATE KEY-----
    <key_value><key_value>
    -----END PRIVATE KEY----------END PRIVATE KEY-----

    备注

    这是稍后将用于配置通知中心的令牌值 。This is the token value that will be used later to configure Notification Hub.

完成这些步骤后,你应具有稍后要在使用 APNS 信息配置通知中心中使用的以下信息:At the end of these steps, you should have the following information for use later in Configure your notification hub with APNS information:

  • 团队 ID(请参阅步骤 1)Team ID (see step 1)
  • 捆绑包 ID(请参阅步骤 1)Bundle ID (see step 1)
  • 密钥 ID(请参阅步骤 7)Key ID (see step 7)
  • 令牌值(在步骤 8 中获取的 p8 密钥值)Token value (p8 key value obtained in step 8)

为应用程序创建配置文件Create a provisioning profile for the app

  1. 返回到 iOS 预配门户,选择“Certificates, Identifiers & Profiles”(证书、标识符和配置文件),从左侧菜单中选择“Profiles”(配置文件),然后选择 + 创建新的配置文件。Return to the iOS Provisioning Portal, select Certificates, Identifiers & Profiles, select Profiles from the left menu, and then select + to create a new profile. 此时将显示“Register a New Provisioning Profile”(注册新的预配配置文件)屏幕。The Register a New Provisioning Profile screen appears.

  2. 选择“Development”(开发)下的“iOS App Development”(iOS 应用程序开发)作为预配配置文件类型,然后选择“Continue”(继续)。 Select iOS App Development under Development as the provisioning profile type, and then select Continue.

    预配配置文件列表

  3. 接下来,从“App ID”(应用 ID)下拉列表中选择创建的应用 ID,然后选择“Continue”(继续)。 Next, select the app ID you created from the App ID drop-down list, and select Continue.

    选择应用 ID

  4. 在“Select certificates”(选择证书)窗口中,选择用于代码签名的开发证书,然后选择“Continue”(继续)。 In the Select certificates window, select the development certificate that you use for code signing, and select Continue.

    备注

    此证书不是在上一步中创建的推送证书。This certificate is not the push certificate you created in the previous step. 这是开发证书。This is your development certificate. 如果没有开发证书,则必须创建一个,因为这是本教程的先决条件If one does not exist, you must create it since this is a prerequisite for this tutorial. 开发人员证书可以在 Apple 开发人员门户中通过 Xcode创建,或在 Visual Studio 中创建。Developer certificates can be created in the Apple Developer Portal, via Xcode or in Visual Studio.

  5. 返回到“Certificates, Identifiers & Profiles”(证书、标识符和配置文件)页,从左侧菜单中选择“Profiles”(配置文件),然后选择 + 创建新的配置文件。Return to the Certificates, Identifiers & Profiles page, select Profiles from the left menu, and then select + to create a new profile. 此时将显示“Register a New Provisioning Profile”(注册新的预配配置文件)屏幕。The Register a New Provisioning Profile screen appears.

  6. 在“选择证书”窗口中,选择你创建的开发证书。In the Select certificates window, select the development certificate that you created. 然后选择“继续”。Then select Continue.

  7. 接下来,选择用于测试的设备,然后选择“Continue”(继续)。Next, select the devices to use for testing, and select Continue.

  8. 最后,在“Provisioning Profile Name”(预配配置文件名称)中为概要文件选择一个名称,然后选择“Generate”(生成)。 Finally, choose a name for the profile in Provisioning Profile Name, and select Generate.

    选择预配配置文件名称

  9. 创建了新的预配配置文件后,选择“Download”(下载)。When the new provisioning profile is created, select Download. 记住保存证书的位置。Remember the location to which it's saved.

  10. 浏览到预配配置文件所在的位置,然后双击该配置文件以将其安装在开发计算机上。Browse to the location of the provisioning profile, and then double-click it to install it on your development machine.

创建通知中心Create a notification hub

本部分将创建一个通知中心,并使用 APNS 配置身份验证。In this section, you create a notification hub and configure authentication with APNS. 可以使用 p12 推送证书或基于令牌的身份验证。You can use a p12 push certificate or token-based authentication. 如果想要使用已创建的通知中心,可以跳到步骤 5。If you want to use a notification hub that you've already created, you can skip to step 5.

  1. 登录到 AzureSign in to Azure.

  2. 单击“创建资源”,搜索并选择“通知中心”,然后单击“创建” 。Click Create a resource, then search for and choose Notification Hub, then click Create.

  3. 更新以下字段,然后单击“创建”:Update the following fields, then click Create:

    基本详细信息BASIC DETAILS

    订阅: 从下拉列表中选择目标订阅Subscription: Choose the target Subscription from the drop-down list
    资源组: 新建资源组(或选择现有资源组)Resource Group: Create a new Resource Group (or pick an existing one)

    命名空间详细信息NAMESPACE DETAILS

    通知中心命名空间: 输入通知中心命名空间的全局唯一名称Notification Hub Namespace: Enter a globally unique name for the Notification Hub namespace

    备注

    确保为此字段选择“新建”选项。Ensure the Create new option is selected for this field.

    通知中心详细信息NOTIFICATION HUB DETAILS

    通知中心: 输入通知中心的名称Notification Hub: Enter a name for the Notification Hub
    位置: 从下拉列表中选择合适的位置Location: Choose a suitable location from the drop-down list
    定价层: 保留默认“免费”选项Pricing Tier: Keep the default Free option

    备注

    除非已达到免费层的最大中心数。Unless you have reached the maximum number of hubs on the free tier.

  4. 预配通知中心后,导航到该资源。Once the Notification Hub has been provisioned, navigate to that resource.

  5. 导航到新的通知中心。Navigate to your new Notification Hub.

  6. 从列表中选择“访问策略”(在“管理”下) 。Select Access Policies from the list (under MANAGE).

  7. 请记下策略名称值及其对应的连接字符串值 。Make note of the Policy Name values along with their corresponding Connection String values.

使用 APNS 信息配置通知中心Configure your notification hub with APNS information

在“Notification Services”下,选择“Apple”,然后根据以前在为通知中心创建证书部分中选择的方法,执行相应的步骤。Under Notification Services, select Apple then follow the appropriate steps based on the approach you chose previously in the Creating a Certificate for Notification Hubs section.

备注

仅当希望将推送通知发送给已从应用商店购买应用的用户时,才应当对“应用程序模式”使用“生产”。Use the Production for Application Mode only if you want to send push notifications to users who purchased your app from the store.

选项 1:使用 .p12 推送证书OPTION 1: Using a .p12 push certificate

  1. 选择“证书”。Select Certificate.

  2. 选择文件图标。Select the file icon.

  3. 选择前面导出的 .p12 文件,然后选择“Open”(打开)。Select the .p12 file that you exported earlier, and then select Open.

  4. 如有必要,请指定正确的密码。If necessary, specify the correct password.

  5. 选择“沙盒”模式。Select Sandbox mode.

  6. 选择“保存”。Select Save.

选项 2:使用基于令牌的身份验证OPTION 2: Using token-based authentication

  1. 选择“令牌”。Select Token.

  2. 输入前面获取的以下值:Enter the following values that you acquired earlier:

    • 密钥 IDKey ID
    • 捆绑包 IDBundle ID
    • 团队 IDTeam ID
    • 令牌Token
  3. 选择“沙盒”。Choose Sandbox.

  4. 选择“保存”。Select Save.

创建 ASP.NET Core Web API 后端应用程序Create an ASP.NET Core Web API backend application

在本部分中,将创建 ASP.NET Core Web API 后端来进行设备注册并将通知发送到 Xamarin.Forms 移动应用。In this section, you create the ASP.NET Core Web API backend to handle device registration and the sending of notifications to the Xamarin.Forms mobile app.

创建 Web 项目Create a web project

  1. 在 Visual Studio 中,选择“文件” > “新建解决方案” 。In Visual Studio, select File > New Solution.

  2. 依次选择“.NET Core” > “应用” > “ASP.NET Core” > “API” > “下一步” 。Select .NET Core > App > ASP.NET Core > API > Next.

  3. 在“配置新的 ASP.NET Core Web API”对话框中,选择 .NET Core 3.1 作为“目标框架” 。In the Configure your new ASP.NET Core Web API dialog, select Target Framework of .NET Core 3.1.

  4. 输入“PushDemoApi”作为项目名称,然后选择“创建” 。Enter PushDemoApi for the Project Name and then select Create.

  5. 启动调试 (Command + Enter) 来测试模板化应用 。Start debugging (Command + Enter) to test the templated app.

    备注

    模板化应用配置为使用 WeatherForecastController 作为 launchUrl。The templated app is configured to use the WeatherForecastController as the launchUrl. 这是在“属性” > “launchSettings.json”中设置的 。This is set in Properties > launchSettings.json.

    如果系统提示“找到的开发证书无效”消息:If you are prompted with an Invalid development certificate found message:

    1. 单击“是”同意运行“dotnet dev-certs https”工具来解决此问题。Click Yes to agree to running the 'dotnet dev-certs https' tool to fix this. 然后“dotnet dev-certs https”工具会提示你输入证书的密码和密钥链的密码。The 'dotnet dev-certs https' tool then prompt you to enter a password for the certificate and the password for your Keychain.

    2. 当系统提示“安装并信任新证书”时,单击“是”,然后输入密钥链的密码 。Click Yes when prompted to Install and trust the new certificate, then enter the password for your Keychain.

  6. 展开“控制器”文件夹,然后删除 WeatherForecastController.cs 。Expand the Controllers folder, then delete WeatherForecastController.cs.

  7. 删除 WeatherForecast.cs。Delete WeatherForecast.cs.

  8. 按住 Control 的同时单击 PushDemoApi 项目,然后从“添加”菜单中选择“新建文件...” 。Control + Click on the PushDemoApi project, then choose New File... from the Add menu.

  9. 使用机密管理器工具设置本地配置值。Set up local configuration values using the Secret Manager tool. 将机密与解决方案分离可确保它们不会终止在源代码管理中。Decoupling the secrets from the solution ensures that they don't end up in source control. 打开“终端”,然后转到项目文件的目录,并运行以下命令:Open Terminal then go to the directory of the project file and run the following commands:

    dotnet user-secrets init
    dotnet user-secrets set "NotificationHub:Name" <value>
    dotnet user-secrets set "NotificationHub:ConnectionString" <value>
    

    将占位符值替换为自己的通知中心名称和连接字符串值。Replace the placeholder values with your own notification hub name and connection string values. 你已在创建通知中心部分中记下了这些值。You made a note of them in the create a notification hub section. 否则,可以在 Azure 中查找这些值。Otherwise, you can look them up in Azure.

    NotificationsHub:NameNotificationsHub:Name:
    请参阅“概述”顶部“基础”摘要中的“名称” 。See Name in the Essentials summary at the top of Overview.

    NotificationHub:ConnectionStringNotificationHub:ConnectionString:
    请参阅“访问策略”中的 DefaultFullSharedAccessSignatureSee DefaultFullSharedAccessSignature in Access Policies

    备注

    对于生产方案,可以查看 Azure KeyVault 等选项,以安全地存储连接字符串。For production scenarios, you can look at options such as Azure KeyVault to securely store the connection string. 为简单起见,机密会添加到 Azure 应用服务应用程序设置。For simplicity, the secrets will be added to the Azure App Service application settings.

使用 API 密钥对客户端进行身份验证(可选)Authenticate clients using an API Key (Optional)

API 密钥的安全性虽然不如令牌,但它也可以满足本教程的需要。API keys aren't as secure as tokens, but will suffice for the purposes of this tutorial. 可通过 ASP.NET 中间件轻松配置 API 密钥。An API key can be configured easily via the ASP.NET Middleware.

  1. 将“API 密钥”添加到本地配置值。Add the API key to the local configuration values.

    dotnet user-secrets set "Authentication:ApiKey" <value>
    

    备注

    应将占位符值替换为你自己的值,并对其进行记录。You should replace the placeholder value with your own and make a note of it.

  2. 按住 Control 的同时单击 PushDemoApi 项目,从“添加”菜单中选择“新建文件夹”,然后单击“添加”,并使用“身份验证”作为文件夹名称 。Control + Click on the PushDemoApi project, choose New Folder from the Add menu, then click Add using Authentication as the Folder Name.

  3. 按住 Control 的同时单击“身份验证”文件夹,然后从“添加”菜单中选择“新建文件...” 。Control + Click on the Authentication folder, then choose New File... from the Add menu.

  4. 选择“常规” > “空类”,输入“ApiKeyAuthOptions.cs”作为名称,然后单击“新建”添加以下实现 。Select General > Empty Class, enter ApiKeyAuthOptions.cs for the Name, then click New adding the following implementation.

    using Microsoft.AspNetCore.Authentication;
    
    namespace PushDemoApi.Authentication
    {
        public class ApiKeyAuthOptions : AuthenticationSchemeOptions
        {
            public const string DefaultScheme = "ApiKey";
            public string Scheme => DefaultScheme;
            public string ApiKey { get; set; }
        }
    }
    
  5. 将另一个“空类”添加到名为 ApiKeyAuthHandler.cs 的“身份验证”文件夹中,然后添加以下实现。Add another Empty Class to the Authentication folder called ApiKeyAuthHandler.cs, then add the following implementation.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Security.Claims;
    using System.Text.Encodings.Web;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authentication;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    
    namespace PushDemoApi.Authentication
    {
        public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
        {
            const string ApiKeyIdentifier = "apikey";
    
            public ApiKeyAuthHandler(
                IOptionsMonitor<ApiKeyAuthOptions> options,
                ILoggerFactory logger,
                UrlEncoder encoder,
                ISystemClock clock)
                : base(options, logger, encoder, clock) {}
    
            protected override Task<AuthenticateResult> HandleAuthenticateAsync()
            {
                string key = string.Empty;
    
                if (Request.Headers[ApiKeyIdentifier].Any())
                {
                    key = Request.Headers[ApiKeyIdentifier].FirstOrDefault();
                }
                else if (Request.Query.ContainsKey(ApiKeyIdentifier))
                {
                    if (Request.Query.TryGetValue(ApiKeyIdentifier, out var queryKey))
                        key = queryKey;
                }
    
                if (string.IsNullOrWhiteSpace(key))
                    return Task.FromResult(AuthenticateResult.Fail("No api key provided"));
    
                if (!string.Equals(key, Options.ApiKey, StringComparison.Ordinal))
                    return Task.FromResult(AuthenticateResult.Fail("Invalid api key."));
    
                var identities = new List<ClaimsIdentity> {
                    new ClaimsIdentity("ApiKeyIdentity")
                };
    
                var ticket = new AuthenticationTicket(
                    new ClaimsPrincipal(identities), Options.Scheme);
    
                return Task.FromResult(AuthenticateResult.Success(ticket));
            }
        }
    }
    

    备注

    身份验证处理程序是实现方案行为的类型,在本例中为自定义 API 密钥方案。An Authentication Handler is a type that implements the behavior of a scheme, in this case a custom API Key scheme.

  6. 将另一个“空类”添加到名为 ApiKeyAuthenticationBuilderExtensions.cs 的“身份验证”文件夹中,然后添加以下实现。Add another Empty Class to the Authentication folder called ApiKeyAuthenticationBuilderExtensions.cs, then add the following implementation.

    using System;
    using Microsoft.AspNetCore.Authentication;
    
    namespace PushDemoApi.Authentication
    {
        public static class AuthenticationBuilderExtensions
        {
            public static AuthenticationBuilder AddApiKeyAuth(
                this AuthenticationBuilder builder,
                Action<ApiKeyAuthOptions> configureOptions)
            {
                return builder
                    .AddScheme<ApiKeyAuthOptions, ApiKeyAuthHandler>(
                        ApiKeyAuthOptions.DefaultScheme,
                        configureOptions);
            }
        }
    }
    

    备注

    此扩展方法简化了 Startup.cs 中的中间件配置代码,使其更具有可读性并且更加通俗易懂。This extension method simplifies the middleware configuration code in Startup.cs making it more readable and generally easier to follow.

  7. 在 Startup.cs 中,更新 ConfigureServices 方法,以便在对 services.AddControllers 方法的调用下配置 API 密钥身份验证 。In Startup.cs, update the ConfigureServices method to configure the API Key authentication below the call to the services.AddControllers method.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
            options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
        }).AddApiKeyAuth(Configuration.GetSection("Authentication").Bind);
    }
    
  8. 继续在 Startup.cs 中更新 Configure 方法,以便对应用的 IApplicationBuilder 调用 UseAuthentication 和 UseAuthorization扩展方法 。Still in Startup.cs, update the Configure method to call the UseAuthentication and UseAuthorization extension methods on the app's IApplicationBuilder. 请确保在 UseRouting 之后且 app.UseEndpoints 之前调用这些方法 。Ensure those methods are called after UseRouting and before app.UseEndpoints.

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
    
        app.UseHttpsRedirection();
    
        app.UseRouting();
    
        app.UseAuthentication();
    
        app.UseAuthorization();
    
        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
        });
    }
    

    备注

    调用 UseAuthentication 将注册中间件,该中间件使用以前注册的身份验证方案(来自 ConfigureServices) 。Calling UseAuthentication registers the middleware which uses the previously registered authentication schemes (from ConfigureServices). 必须在依赖于要进行身份验证的用户的任何中间件之前,调用此方法。This must be called before any middleware that depends on users being authenticated.

添加依赖项并配置服务Add Dependencies and Configure Services

ASP.NET Core 支持依赖项注入 (DI) 软件设计模式,这是一种在类与其依赖项之间实现控制反转 (IoC) 的方法。ASP.NET Core supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.

使用的通知中心和用于后端操作的通知中心 SDK 封装在服务中。Use of the notification hub and the Notification Hubs SDK for backend operations is encapsulated within a service. 该服务通过适当的抽象进行注册和提供。The service is registered and made available through a suitable abstraction.

  1. 按住 Control 的同时单击“依赖项”文件夹,然后选择“管理 NuGet 包...” 。Control + Click on the Dependencies folder, then choose Manage NuGet Packages....

  2. 搜索 Microsoft.Azure.NotificationHubs 并确保选中它。Search for Microsoft.Azure.NotificationHubs and ensure it's checked.

  3. 单击“添加包”,然后在系统提示接受许可条款时单击“接受” 。Click Add Packages, then click Accept when prompted to accept the license terms.

  4. 按住 Control 的同时单击 PushDemoApi 项目,从“添加”菜单中选择“新建文件夹”,然后单击“添加”并使用“模型”作为文件夹名称 。Control + Click on the PushDemoApi project, choose New Folder from the Add menu, then click Add using Models as the Folder Name.

  5. 按住 Control 的同时单击“模型”文件夹,然后从“添加”菜单中选择“新建文件...” 。Control + Click on the Models folder, then choose New File... from the Add menu.

  6. 选择“常规” > “空类”,输入 PushTemplates.cs 作为名称,然后单击“新建”添加以下实现 。Select General > Empty Class, enter PushTemplates.cs for the Name, then click New adding the following implementation.

    namespace PushDemoApi.Models
    {
        public class PushTemplates
        {
            public class Generic
            {
                public const string Android = "{ \"notification\": { \"title\" : \"PushDemo\", \"body\" : \"$(alertMessage)\"}, \"data\" : { \"action\" : \"$(alertAction)\" } }";
                public const string iOS = "{ \"aps\" : {\"alert\" : \"$(alertMessage)\"}, \"action\" : \"$(alertAction)\" }";
            }
    
            public class Silent
            {
                public const string Android = "{ \"data\" : {\"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\"} }";
                public const string iOS = "{ \"aps\" : {\"content-available\" : 1, \"apns-priority\": 5, \"sound\" : \"\", \"badge\" : 0}, \"message\" : \"$(alertMessage)\", \"action\" : \"$(alertAction)\" }";
            }
        }
    }
    

    备注

    此类包含此方案所需的泛型和无提示通知的标记化通知有效负载。This class contains the tokenized notification payloads for the generic and silent notifications required by this scenario. 有效负载在安装外定义,以便无需通过服务更新现有安装即可进行试验。The payloads are defined outside of the Installation to allow experimentation without having to update existing installations via the service. 以这种方式处理对安装的更改超出了本教程的范围。Handling changes to installations in this way is out of scope for this tutorial. 对于生产环境,请考虑使用自定义模板For production, consider custom templates.

  7. 选择“常规” > “空类”,输入 DeviceInstallation.cs 作为“名称”,然后单击“新建”添加以下实现 。Select General > Empty Class, enter DeviceInstallation.cs for the Name, then click New adding the following implementation.

    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class DeviceInstallation
        {
            [Required]
            public string InstallationId { get; set; }
    
            [Required]
            public string Platform { get; set; }
    
            [Required]
            public string PushChannel { get; set; }
    
            public IList<string> Tags { get; set; } = Array.Empty<string>();
        }
    }
    
  8. 将另一个“空类”添加到名为 NotificationRequest.cs 的“模型”文件夹中,然后添加以下实现。Add another Empty Class to the Models folder called NotificationRequest.cs, then add the following implementation.

    using System;
    
    namespace PushDemoApi.Models
    {
        public class NotificationRequest
        {
            public string Text { get; set; }
            public string Action { get; set; }
            public string[] Tags { get; set; } = Array.Empty<string>();
            public bool Silent { get; set; }
        }
    }
    
  9. 将另一个“空类”添加到名为 NotificationHubOptions.cs 的“模型”文件夹中,然后添加以下实现 。Add another Empty Class to the Models folder called NotificationHubOptions.cs, then add the following implementation.

    using System.ComponentModel.DataAnnotations;
    
    namespace PushDemoApi.Models
    {
        public class NotificationHubOptions
        {
            [Required]
            public string Name { get; set; }
    
            [Required]
            public string ConnectionString { get; set; }
        }
    }
    
  10. 将一个新文件夹添加到名为“服务”的 PushDemoApi 项目。Add a new folder to the PushDemoApi project called Services.

  11. 将“空接口”添加到名为 INotificationService.cs 的“服务”文件夹,然后添加以下实现 。Add an Empty Interface to the Services folder called INotificationService.cs, then add the following implementation.

    using System.Threading.Tasks;
    using PushDemoApi.Models;
    
    namespace PushDemoApi.Services
    {
        public interface INotificationService
        {
            Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token);
            Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token);
            Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token);
        }
    }
    
  12. 将“空类”添加到名为 NotificationHubsService.cs 的“服务”文件夹中,然后添加以下代码以实现 INotificationService 接口 :Add an Empty Class to the Services folder called NotificationHubsService.cs, then add the following code to implement the INotificationService interface:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.Azure.NotificationHubs;
    using Microsoft.Extensions.Logging;
    using Microsoft.Extensions.Options;
    using PushDemoApi.Models;
    
    namespace PushDemoApi.Services
    {
        public class NotificationHubService : INotificationService
        {
            readonly NotificationHubClient _hub;
            readonly Dictionary<string, NotificationPlatform> _installationPlatform;
            readonly ILogger<NotificationHubService> _logger;
    
            public NotificationHubService(IOptions<NotificationHubOptions> options, ILogger<NotificationHubService> logger)
            {
                _logger = logger;
                _hub = NotificationHubClient.CreateClientFromConnectionString(
                    options.Value.ConnectionString,
                    options.Value.Name);
    
                _installationPlatform = new Dictionary<string, NotificationPlatform>
                {
                    { nameof(NotificationPlatform.Apns).ToLower(), NotificationPlatform.Apns },
                    { nameof(NotificationPlatform.Fcm).ToLower(), NotificationPlatform.Fcm }
                };
            }
    
            public async Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation deviceInstallation, CancellationToken token)
            {
                if (string.IsNullOrWhiteSpace(deviceInstallation?.InstallationId) ||
                    string.IsNullOrWhiteSpace(deviceInstallation?.Platform) ||
                    string.IsNullOrWhiteSpace(deviceInstallation?.PushChannel))
                    return false;
    
                var installation = new Installation()
                {
                    InstallationId = deviceInstallation.InstallationId,
                    PushChannel = deviceInstallation.PushChannel,
                    Tags = deviceInstallation.Tags
                };
    
                if (_installationPlatform.TryGetValue(deviceInstallation.Platform, out var platform))
                    installation.Platform = platform;
                else
                    return false;
    
                try
                {
                    await _hub.CreateOrUpdateInstallationAsync(installation, token);
                }
                catch
                {
                    return false;
                }
    
                return true;
            }
    
            public async Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken token)
            {
                if (string.IsNullOrWhiteSpace(installationId))
                    return false;
    
                try
                {
                    await _hub.DeleteInstallationAsync(installationId, token);
                }
                catch
                {
                    return false;
                }
    
                return true;
            }
    
            public async Task<bool> RequestNotificationAsync(NotificationRequest notificationRequest, CancellationToken token)
            {
                if ((notificationRequest.Silent &&
                    string.IsNullOrWhiteSpace(notificationRequest?.Action)) ||
                    (!notificationRequest.Silent &&
                    (string.IsNullOrWhiteSpace(notificationRequest?.Text)) ||
                    string.IsNullOrWhiteSpace(notificationRequest?.Action)))
                    return false;
    
                var androidPushTemplate = notificationRequest.Silent ?
                    PushTemplates.Silent.Android :
                    PushTemplates.Generic.Android;
    
                var iOSPushTemplate = notificationRequest.Silent ?
                    PushTemplates.Silent.iOS :
                    PushTemplates.Generic.iOS;
    
                var androidPayload = PrepareNotificationPayload(
                    androidPushTemplate,
                    notificationRequest.Text,
                    notificationRequest.Action);
    
                var iOSPayload = PrepareNotificationPayload(
                    iOSPushTemplate,
                    notificationRequest.Text,
                    notificationRequest.Action);
    
                try
                {
                    if (notificationRequest.Tags.Length == 0)
                    {
                        // This will broadcast to all users registered in the notification hub
                        await SendPlatformNotificationsAsync(androidPayload, iOSPayload, token);
                    }
                    else if (notificationRequest.Tags.Length <= 20)
                    {
                        await SendPlatformNotificationsAsync(androidPayload, iOSPayload, notificationRequest.Tags, token);
                    }
                    else
                    {
                        var notificationTasks = notificationRequest.Tags
                            .Select((value, index) => (value, index))
                            .GroupBy(g => g.index / 20, i => i.value)
                            .Select(tags => SendPlatformNotificationsAsync(androidPayload, iOSPayload, tags, token));
    
                        await Task.WhenAll(notificationTasks);
                    }
    
                    return true;
                }
                catch (Exception e)
                {
                    _logger.LogError(e, "Unexpected error sending notification");
                    return false;
                }
            }
    
            string PrepareNotificationPayload(string template, string text, string action) => template
                .Replace("$(alertMessage)", text, StringComparison.InvariantCulture)
                .Replace("$(alertAction)", action, StringComparison.InvariantCulture);
    
            Task SendPlatformNotificationsAsync(string androidPayload, string iOSPayload, CancellationToken token)
            {
                var sendTasks = new Task[]
                {
                    _hub.SendFcmNativeNotificationAsync(androidPayload, token),
                    _hub.SendAppleNativeNotificationAsync(iOSPayload, token)
                };
    
                return Task.WhenAll(sendTasks);
            }
    
            Task SendPlatformNotificationsAsync(string androidPayload, string iOSPayload, IEnumerable<string> tags, CancellationToken token)
            {
                var sendTasks = new Task[]
                {
                    _hub.SendFcmNativeNotificationAsync(androidPayload, tags, token),
                    _hub.SendAppleNativeNotificationAsync(iOSPayload, tags, token)
                };
    
                return Task.WhenAll(sendTasks);
            }
        }
    }
    

    备注

    提供给 SendTemplateNotificationAsync 的标记表达式限制为包含 20 个标记。The tag expression provided to SendTemplateNotificationAsync is limited to 20 tags. 大多数运算符的限制数量为 6 个,但在本例中,表达式仅包含 OR (||)。It is limited to 6 for most operators but the expression contains only ORs (||) in this case. 如果请求中的标记超过 20 个,则必须将它们拆分为多个请求。If there are more than 20 tags in the request then they must be split into multiple requests. 有关更多详细信息,请参阅路由和标记表达式文档。See the Routing and Tag Expressions documentation for more detail.

  13. 在 Startup.cs 中,更新 ConfigureServices 方法,以将 NotificationHubsService 添加为 INotificationService 的单一实现 。In Startup.cs, update the ConfigureServices method to add the NotificationHubsService as a singleton implementation of INotificationService.

    public void ConfigureServices(IServiceCollection services)
    {
        ...
    
        services.AddSingleton<INotificationService, NotificationHubService>();
    
        services.AddOptions<NotificationHubOptions>()
            .Configure(Configuration.GetSection("NotificationHub").Bind)
            .ValidateDataAnnotations();
    }
    

创建通知 APICreate the Notifications API

  1. 按住 Control 的同时单击“控制器”文件夹,然后从“添加”菜单中选择“新建文件...” 。Control + Click on the Controllers folder, then choose New File... from the Add menu.

  2. 选择“ASP.NET Core” > “Web API 控制器类”,输入 NotificationsController 作为名称,然后单击“新建” 。Select ASP.NET Core > Web API Controller Class, enter NotificationsController for the Name, then click New.

  3. 将以下命名空间添加到文件顶部。Add the following namespaces to the top of the file.

    using System.ComponentModel.DataAnnotations;
    using System.Net;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    
  4. 更新模板化控制器,使其从 ControllerBase 派生,并通过 ApiController 属性进行修饰 。Update the templated controller so it derives from ControllerBase and is decorated with the ApiController attribute.

    [ApiController]
    [Route("api/[controller]")]
    public class NotificationsController : ControllerBase
    {
        // Templated methods here
    }
    

    备注

    “控制器”基类为视图提供支持,但本例中不需要它,因此可以改为使用 ControllerBase 。The Controller base class provides support support for views but this is not needed in this case and so ControllerBase can be used instead.

  5. 如果选择完成使用 API 密钥对客户端进行身份验证部分,则还应该使用 Authorize 特性来修饰 NotificationsController 。If you chose to complete the Authenticate clients using an API Key section, you should decorate the NotificationsController with the Authorize attribute as well.

    [Authorize]
    
  6. 更新构造函数以接受 INotificationServic 的注册实例作为参数,并将其分配给只读成员。Update the constructor to accept the registered instance of INotificationService as an argument and assign it to a readonly member.

    readonly INotificationService _notificationService;
    
    public NotificationsController(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
    
  7. 在 launchSettings.json(位于“属性”文件夹内)中,将 launchUrl 从 weatherforecast 更改为 api/notifications,以匹配在 RegistrationsController Route 特性中指定的 URL 。In launchSettings.json (within the Properties folder), change the launchUrl from weatherforecast to api/notifications to match the URL specified in the RegistrationsController Route attribute.

  8. 启动调试 (Command + Enter),验证应用是否正在使用新的 NotificationsController 并返回“401 未授权”状态 。Start debugging (Command + Enter) to validate the app is working with the new NotificationsController and returns a 401 Unauthorized status.

    备注

    Visual Studio 可能不会在浏览器中自动启动该应用。Visual Studio may not automatically launch the app in the browser. 此时将使用 Postman 来测试 API。You will use Postman to test the API from this point on.

  9. 在新的 Postman 选项卡上,将请求设置为“GET”并在下面输入地址 。On a new Postman tab, set the request to GET and enter the address below.

    https://localhost:5001/api/notifications
    

    备注

    Localhost 地址应与“属性” > “launchSettings.json”中找到的 applicationUrl 值匹配 。The localhost address should match the applicationUrl value found in Properties > launchSettings.json. 默认值应为 https://localhost:5001;http://localhost:5000,但是如果收到 404 响应,则需要验证这一点。The default should be https://localhost:5001;http://localhost:5000 however this is something to verify if you receive a 404 response.

  10. 如果选择完成使用 API 密钥对客户端进行身份验证部分,请确保将请求标头配置为包含 apikey 值。If you chose to complete the Authenticate clients using an API Key section, be sure to configure the request headers to include your apikey value.

    密钥Key Value
    apikeyapikey <your_api_key><your_api_key>
  11. 单击“发送”按钮。Click the Send button.

    备注

    你应收到“200 OK”状态,其中包含一些 JSON 内容 。You should receive a 200 OK status with some JSON content.

    如果收到“SSL 证书验证”警告,则可以在“设置”中关闭请求 SSL 证书验证 Postman 设置 。If you receive an SSL certificate verification warning, you can switch the request SSL certificate verification Postman setting off in the Settings.

  12. 将模板化类方法替换为以下代码。Replace the templated class methods with the following code.

    [HttpPut]
    [Route("installations")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
    public async Task<IActionResult> UpdateInstallation(
        [Required]DeviceInstallation deviceInstallation)
    {
        var success = await _notificationService
            .CreateOrUpdateInstallationAsync(deviceInstallation, HttpContext.RequestAborted);
    
        if (!success)
            return new UnprocessableEntityResult();
    
        return new OkResult();
    }
    
    [HttpDelete()]
    [Route("installations/{installationId}")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
    public async Task<ActionResult> DeleteInstallation(
        [Required][FromRoute]string installationId)
    {
        var success = await _notificationService
            .DeleteInstallationByIdAsync(installationId, CancellationToken.None);
    
        if (!success)
            return new UnprocessableEntityResult();
    
        return new OkResult();
    }
    
    [HttpPost]
    [Route("requests")]
    [ProducesResponseType((int)HttpStatusCode.OK)]
    [ProducesResponseType((int)HttpStatusCode.BadRequest)]
    [ProducesResponseType((int)HttpStatusCode.UnprocessableEntity)]
    public async Task<IActionResult> RequestPush(
        [Required]NotificationRequest notificationRequest)
    {
        if ((notificationRequest.Silent &&
            string.IsNullOrWhiteSpace(notificationRequest?.Action)) ||
            (!notificationRequest.Silent &&
            string.IsNullOrWhiteSpace(notificationRequest?.Text)))
            return new BadRequestResult();
    
        var success = await _notificationService
            .RequestNotificationAsync(notificationRequest, HttpContext.RequestAborted);
    
        if (!success)
            return new UnprocessableEntityResult();
    
        return new OkResult();
    }
    

创建 API 应用Create the API App

现在,在 Azure 应用服务中创建 API 应用,以便托管后端服务。You now create an API App in Azure App Service for hosting the backend service.

  1. 登录 Azure 门户Sign in to the Azure portal.

  2. 单击“创建资源”,搜索并选择“API 应用”,然后单击“创建” 。Click Create a resource, then search for and choose API App, then click Create.

  3. 更新以下字段,然后单击“创建”。Update the following fields, then click Create.

    应用名称:App name:
    输入 API 应用的全局唯一名称Enter a globally unique name for the API App

    订阅:Subscription:
    选择在其中创建了通知中心的同一目标订阅。Choose the same target Subscription you created the notification hub in.

    资源组:Resource Group:
    选中在其中创建了通知中心的同一资源组。Choose the same Resource Group you created the notification hub in.

    应用服务计划/位置:App Service Plan/Location:
    创建新的应用服务计划Create a new App Service Plan

    备注

    将默认选项更改为包含 SSL 支持的计划。Change from the default option to a plan that includes SSL support. 否则,在使用移动应用时,需要执行适当的步骤,以防止 http 请求被阻止。Otherwise, you will need to take the appropriate steps when working with the mobile app to prevent http requests from getting blocked.

    Application Insights:Application Insights:
    保留建议的选项(将使用该名称创建新资源)或选择现有资源。Keep the suggested option (a new resource will be created using that name) or pick an existing resource.

  4. 预配 API 应用后,导航到该资源。Once the API App has been provisioned, navigate to that resource.

  5. 记下“概述”顶部“基础信息”摘要中的 URL 属性 。Make note of the URL property in the Essentials summary at the top of the Overview. 此 URL 是“后端终结点”,本教程后面的部分中会用到它。This URL is your backend endpoint that will be used later in this tutorial.

    备注

    URL 使用前面指定的 API 应用名称,其格式为 https://<app_name>.chinacloudsites.cnThe URL uses the API app name that you specified earlier, with the format https://<app_name>.chinacloudsites.cn.

  6. 从列表选择“配置”(在“设置”下) 。Select Configuration from the list (under Settings).

  7. 对于下面的每个设置,单击“新建应用程序设置”,输入名称和值,然后单击“确定” 。For each of the settings below, click New application setting to enter the Name and a Value, then click OK.

    名称Name Value
    Authentication:ApiKey <api_key_value><api_key_value>
    NotificationHub:Name <hub_name_value><hub_name_value>
    NotificationHub:ConnectionString <hub_connection_string_value><hub_connection_string_value>

    备注

    这些设置与你之前在用户设置中定义的设置相同。These are the same settings you defined previously in the user settings. 应该可以复制将这些设置复制过来。You should be able to copy these over. 仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 Authentication:ApiKey 设置。The Authentication:ApiKey setting is required only if you chose to to complete the Authenticate clients using an API Key section. 对于生产方案,可以查看 Azure KeyVault 等选项。For production scenarios, you can look at options such as Azure KeyVault. 在本例中,为简单起见,这些设置已作为应用程序设置添加。These have been added as application settings for simplicity in this case.

  8. 添加所有应用程序设置后,依次单击“保存”和“继续” 。Once all application settings have been added click Save, then Continue.

发布后端服务Publish the backend service

接下来,需要将应用部署到 API 应用,以便可以从任意设备访问它。Next, you deploy the app to the API App to make it accessible from all devices.

  1. 将配置从“调试”更改为“发布”(如果尚未这样做) 。Change your configuration from Debug to Release if you haven't already done so.

  2. 按住 Control 的同时单击 PushDemoApi 项目,然后从“发布”菜单中选择“发布到 Azure...” 。Control + Click the PushDemoApi project, and then choose Publish to Azure... from the Publish menu.

  3. 如果系统提示进行身份验证,请遵循身份验证流。Follow the auth flow if prompted to do so. 使用之前在创建 API 应用部分中使用的帐户。Use the account that you used in the previous create the API App section.

  4. 从列表中选择之前创建的“Azure 应用服务 API 应用”作为发布目标,然后单击“发布” 。Select the Azure App Service API App you created previously from the list as your publish target, and then click Publish.

完成向导后,它会将应用发布到 Azure,然后打开该应用。After you've completed the wizard, it publishes the app to Azure and then opens the app. 记下 URL(如果尚未这样做)。Make a note of the URL if you haven't done so already. 此 URL 是“后端终结点”,本教程后面的部分中会用到它。This URL is your backend endpoint that is used later in this tutorial.

验证已发布的 APIValidating the published API

  1. Postman 中打开新选项卡,将请求设置为“POST”并在下面输入地址 。In Postman open a new tab, set the request to POST and enter the address below. 将占位符替换为在之前的发布后端服务部分中记下的基址。Replace the placeholder with the base address you made note of in the previous publish the backend service section.

    https://<app_name>.chinacloudsites.cn/api/notifications/installations
    

    备注

    基址的格式应为 https://<app_name>.chinacloudsites.cn/The base address should be in the format https://<app_name>.chinacloudsites.cn/

  2. 如果选择完成使用 API 密钥对客户端进行身份验证部分,请确保将请求标头配置为包含 apikey 值。If you chose to complete the Authenticate clients using an API Key section, be sure to configure the request headers to include your apikey value.

    密钥Key Value
    apikeyapikey <your_api_key><your_api_key>
  3. 为“正文”选择“原始”选项,接下来从格式选项列表中选择“JSON”,然后包含一些占位符 JSON 内容 :Choose the raw option for the Body, then choose JSON from the list of format options, and then include some placeholder JSON content:

    {}
    
  4. 单击“Send”。Click Send.

    备注

    你应收到来自服务的“400 错误请求”状态。You should receive a 400 Bad Request status from the service.

  5. 再次执行步骤 1-4,但这次指定请求终结点以验证是否收到相同的“400 错误请求”响应。Do steps 1-4 again but this time specifying the requests endpoint to validate you receive the same 400 Bad Request response.

    https://<app_name>.chinacloudsites.cn/api/notifications/requests
    

备注

尚无法使用有效的请求数据测试 API,因为此操作需要来自客户端移动应用的特定于平台的信息。It is not yet possible to test the API using valid request data since this will require platform-specific information from the client mobile app.

创建跨平台 Xamarin.Forms 应用程序Create a cross-platform Xamarin.Forms application

在本部分中,将构建一个 Xamarin.Forms 移动应用程序,实施以跨平台的方式推送通知。In this section, you build a Xamarin.Forms mobile application implementing push notifications in a cross-platform manner.

它允许你通过创建的后端服务从通知中心注册和取消注册。It enables you to register and deregister from a notification hub via the backend service that you created.

当指定了一个操作,并且应用位于前台时,将显示一个警报。An alert is displayed when an action is specified and the app is in the foreground. 否则,通知中心将显示通知。Otherwise, notifications appear in notification center.

备注

通常,会在应用程序生命周期中的相应时间点(或在首次运行体验过程中)执行注册(和取消注册)操作,无需进行显式用户注册/取消注册输入。You would typically perform the registration (and deregistration) actions during the appropriate point in the application lifecycle (or as part of your first-run experience perhaps) without explicit user register/deregister inputs. 但是,此示例将需要显式用户输入,以便能够更轻松地探索和测试此功能。However, this example will require explicit user input to allow this functionality to be explored and tested more easily.

创建 Xamarin.Forms 解决方案Create the Xamarin.Forms solution

  1. 在 Visual Studio 中,使用“空白窗体应用”作为模板创建一个新的 Xamarin.Forms 解决方案,并输入 PushDemo 作为项目名称 。In Visual Studio, create a new Xamarin.Forms solution using Blank Forms App as the template and entering PushDemo for the Project Name.

    备注

    在“配置空白窗体应用”对话框中,确保“组织标识符”与之前使用的值相匹配,并确保同时选中 Android 和 iOS 目标 。In the Configure your Blank Forms App dialog, ensure the Organization Identifier matches the value you used previously and that both Android and iOS targets are checked.

  2. 按住 Control 的同时单击 PushDemo 解决方案,然后选择“更新 NuGet 包” 。Control + Click on the PushDemo solution, then choose Update NuGet Packages.

  3. 按住 Control 的同时单击 PushDemo 解决方案,然后选择“管理 NuGet 包...” 。Control + Click on the PushDemo solution, then choose Manage NuGet Packages...

  4. 搜索“Newtonsoft.Json”并确保将其选中。Search for Newtonsoft.Json and ensure it's checked.

  5. 单击“添加包”,然后在系统提示接受许可条款时单击“接受” 。Click Add Packages, then click Accept when prompted to accept the license terms.

  6. 在每个目标平台上构建并运行应用 (Command + Enter),以测试模板化应用在设备上的运行情况 。Build and run the app on each target platform (Command + Enter) to test the templated app runs on your device(s).

实现跨平台组件Implement the cross-platform components

  1. 按住 Control 的同时单击 PushDemo 项目,从“添加”菜单中选择“新建文件夹”,然后单击“添加”并使用“模型”作为“文件夹名称” 。Control + Click on the PushDemo project, choose New Folder from the Add menu, then click Add using Models as the Folder Name.

  2. 按住 Control 的同时单击“模型”文件夹,然后从“添加”菜单中选择“新建文件...” 。Control + Click on the Models folder, then choose New File... from the Add menu.

  3. 选择“常规” > “空类”,输入 DeviceInstallation.cs,然后添加以下实现 。Select General > Empty Class, enter DeviceInstallation.cs, then add the following implementation.

    using System.Collections.Generic;
    using Newtonsoft.Json;
    
    namespace PushDemo.Models
    {
        public class DeviceInstallation
        {
            [JsonProperty("installationId")]
            public string InstallationId { get; set; }
    
            [JsonProperty("platform")]
            public string Platform { get; set; }
    
            [JsonProperty("pushChannel")]
            public string PushChannel { get; set; }
    
            [JsonProperty("tags")]
            public List<string> Tags { get; set; } = new List<string>();
        }
    }
    
  4. 使用以下实现将“空枚举”添加到名为 PushDemoAction.cs 的“模型”文件夹 。Add an Empty Enumeration to the Models folder called PushDemoAction.cs with the following implementation.

    namespace PushDemo.Models
    {
        public enum PushDemoAction
        {
            ActionA,
            ActionB
        }
    }
    
  5. 将新文件夹添加到名为“服务”的 PushDemo 项目,然后使用以下实现将“空类”添加到名为 ServiceContainer.cs 的文件夹。Add a new folder to the PushDemo project called Services then add an Empty Class to that folder called ServiceContainer.cs with the following implementation.

    using System;
    using System.Collections.Generic;
    
    namespace PushDemo.Services
    {
       public static class ServiceContainer
       {
           static readonly Dictionary<Type, Lazy<object>> services
               = new Dictionary<Type, Lazy<object>>();
    
           public static void Register<T>(Func<T> function)
               => services[typeof(T)] = new Lazy<object>(() => function());
    
           public static T Resolve<T>()
               => (T)Resolve(typeof(T));
    
           public static object Resolve(Type type)
           {
               {
                   if (services.TryGetValue(type, out var service))
                       return service.Value;
    
                   throw new KeyNotFoundException($"Service not found for type '{type}'");
               }
           }
       }
    }
    

    备注

    这是 XamCAT 存储库中 ServiceContainer 类的精简版本。This is a trimmed-down version of the ServiceContainer class from the XamCAT repository. 它将用作轻量 IoC(控制反转)容器。It will be used as a light-weight IoC (Inversion of Control) container.

  6. 将“空接口”添加到名为 IDeviceInstallationService.cs 的“服务”文件夹中,然后添加以下代码。Add an Empty Interface to the Services folder called IDeviceInstallationService.cs, then add the following code.

    using PushDemo.Models;
    
    namespace PushDemo.Services
    {
        public interface IDeviceInstallationService
        {
            string Token { get; set; }
            bool NotificationsSupported { get; }
            string GetDeviceId();
            DeviceInstallation GetDeviceInstallation(params string[] tags);
        }
    }
    

    备注

    稍后将由每个目标实现并引导此接口,以提供特定于平台的功能和后端服务所需的 DeviceInstallation 信息。This interface will be implemented and bootstrapped by each target later to provide the platform-specific functionality and DeviceInstallation information required by the backend service.

  7. 将另一个“空接口”添加到名为 INotificationRegistrationService.cs 的“服务”文件夹中,然后添加以下代码 。Add another Empty Interface to the Services folder called INotificationRegistrationService.cs, then add the following code.

    using System.Threading.Tasks;
    
    namespace PushDemo.Services
    {
        public interface INotificationRegistrationService
        {
            Task DeregisterDeviceAsync();
            Task RegisterDeviceAsync(params string[] tags);
            Task RefreshRegistrationAsync();
        }
    }
    

    备注

    这将处理客户端和后端服务之间的交互。This will handle the interaction between the client and backend service.

  8. 将另一个“空接口”添加到名为 INotificationActionService.cs 的服务”文件夹中,然后添加以下代码 。Add another Empty Interface to the Services folder called INotificationActionService.cs, then add the following code.

    namespace PushDemo.Services
    {
        public interface INotificationActionService
        {
            void TriggerAction(string action);
        }
    }
    

    备注

    这是集中处理通知操作的简单机制。This is used as a simple mechanism to centralize the handling of notification actions.

  9. 使用以下实现将“空接口”添加到名为 IPushDemoNotificationActionService.cs 的“服务”文件夹,该文件夹派生自 INotificationActionService 。Add an Empty Interface to the Services folder called IPushDemoNotificationActionService.cs that derives from the INotificationActionService, with the following implementation.

    using System;
    using PushDemo.Models;
    
    namespace PushDemo.Services
    {
        public interface IPushDemoNotificationActionService : INotificationActionService
        {
            event EventHandler<PushDemoAction> ActionTriggered;
        }
    }
    

    备注

    此类型特定于 PushDemo 应用程序,并使用 PushDemoAction 枚举来标识以强类型方式触发的操作 。This type is specific to the PushDemo application and uses the PushDemoAction enumeration to identify the action that is being triggered in a strongly-typed manner.

  10. 将“空类”添加到名为 NotificationRegistrationService.cs 的“服务”文件夹,并使用以下代码实现 INotificationRegistrationService 。Add an Empty Class to the Services folder called NotificationRegistrationService.cs implementing the INotificationRegistrationService with the following code.

    using System;
    using System.Net.Http;
    using System.Text;
    using System.Threading.Tasks;
    using Newtonsoft.Json;
    using PushDemo.Models;
    using Xamarin.Essentials;
    
    namespace PushDemo.Services
    {
        public class NotificationRegistrationService : INotificationRegistrationService
        {
            const string RequestUrl = "api/notifications/installations";
            const string CachedDeviceTokenKey = "cached_device_token";
            const string CachedTagsKey = "cached_tags";
    
            string _baseApiUrl;
            HttpClient _client;
            IDeviceInstallationService _deviceInstallationService;
    
            public NotificationRegistrationService(string baseApiUri, string apiKey)
            {
                _client = new HttpClient();
                _client.DefaultRequestHeaders.Add("Accept", "application/json");
                _client.DefaultRequestHeaders.Add("apikey", apiKey);
    
                _baseApiUrl = baseApiUri;
            }
    
            IDeviceInstallationService DeviceInstallationService
                => _deviceInstallationService ??
                    (_deviceInstallationService = ServiceContainer.Resolve<IDeviceInstallationService>());
    
            public async Task DeregisterDeviceAsync()
            {
                var cachedToken = await SecureStorage.GetAsync(CachedDeviceTokenKey)
                    .ConfigureAwait(false);
    
                if (cachedToken == null)
                    return;
    
                var deviceId = DeviceInstallationService?.GetDeviceId();
    
                if (string.IsNullOrWhiteSpace(deviceId))
                    throw new Exception("Unable to resolve an ID for the device.");
    
                await SendAsync(HttpMethod.Delete, $"{RequestUrl}/{deviceId}")
                    .ConfigureAwait(false);
    
                SecureStorage.Remove(CachedDeviceTokenKey);
                SecureStorage.Remove(CachedTagsKey);
            }
    
            public async Task RegisterDeviceAsync(params string[] tags)
            {
                var deviceInstallation = DeviceInstallationService?.GetDeviceInstallation(tags);
    
                await SendAsync<DeviceInstallation>(HttpMethod.Put, RequestUrl, deviceInstallation)
                    .ConfigureAwait(false);
    
                await SecureStorage.SetAsync(CachedDeviceTokenKey, deviceInstallation.PushChannel)
                    .ConfigureAwait(false);
    
                await SecureStorage.SetAsync(CachedTagsKey, JsonConvert.SerializeObject(tags));
            }
    
            public async Task RefreshRegistrationAsync()
            {
                var cachedToken = await SecureStorage.GetAsync(CachedDeviceTokenKey)
                    .ConfigureAwait(false);
    
                var serializedTags = await SecureStorage.GetAsync(CachedTagsKey)
                    .ConfigureAwait(false);
    
                if (string.IsNullOrWhiteSpace(cachedToken) ||
                    string.IsNullOrWhiteSpace(serializedTags) ||
                    string.IsNullOrWhiteSpace(DeviceInstallationService.Token) ||
                    cachedToken == DeviceInstallationService.Token)
                    return;
    
                var tags = JsonConvert.DeserializeObject<string[]>(serializedTags);
    
                await RegisterDeviceAsync(tags);
            }
    
            async Task SendAsync<T>(HttpMethod requestType, string requestUri, T obj)
            {
                string serializedContent = null;
    
                await Task.Run(() => serializedContent = JsonConvert.SerializeObject(obj))
                    .ConfigureAwait(false);
    
                await SendAsync(requestType, requestUri, serializedContent);
            }
    
            async Task SendAsync(
                HttpMethod requestType,
                string requestUri,
                string jsonRequest = null)
            {
                var request = new HttpRequestMessage(requestType, new Uri($"{_baseApiUrl}{requestUri}"));
    
                if (jsonRequest != null)
                    request.Content = new StringContent(jsonRequest, Encoding.UTF8, "application/json");
    
                var response = await _client.SendAsync(request).ConfigureAwait(false);
    
                response.EnsureSuccessStatusCode();
            }
        }
    }
    

    备注

    仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 apiKey 参数。The apiKey argument is only required if you chose to complete the Authenticate clients using an API Key section.

  11. 将“空类”添加到名为 PushDemoNotificationActionService.cs 的“服务”文件夹,并使用以下代码实现 IPushDemoNotificationActionService 。Add an Empty Class to the Services folder called PushDemoNotificationActionService.cs implementing the IPushDemoNotificationActionService with the following code.

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using PushDemo.Models;
    
    namespace PushDemo.Services
    {
        public class PushDemoNotificationActionService : IPushDemoNotificationActionService
        {
            readonly Dictionary<string, PushDemoAction> _actionMappings = new Dictionary<string, PushDemoAction>
            {
                { "action_a", PushDemoAction.ActionA },
                { "action_b", PushDemoAction.ActionB }
            };
    
            public event EventHandler<PushDemoAction> ActionTriggered = delegate { };
    
            public void TriggerAction(string action)
            {
                if (!_actionMappings.TryGetValue(action, out var pushDemoAction))
                    return;
    
                List<Exception> exceptions = new List<Exception>();
    
                foreach (var handler in ActionTriggered?.GetInvocationList())
                {
                    try
                    {
                        handler.DynamicInvoke(this, pushDemoAction);
                    }
                    catch (Exception ex)
                    {
                        exceptions.Add(ex);
                    }
                }
    
                if (exceptions.Any())
                    throw new AggregateException(exceptions);
            }
        }
    }
    
  12. 使用以下实现将“空类”添加到名为 Config.cs 的 PushDemo 项目 。Add an Empty Class to the PushDemo project called Config.cs with the following implementation.

    namespace PushDemo
    {
        public static partial class Config
        {
            public static string ApiKey = "API_KEY";
            public static string BackendServiceEndpoint = "BACKEND_SERVICE_ENDPOINT";
        }
    }
    

    备注

    这是将机密从源代码管理中排除的简单方法。This is used as a simple way to keep secrets out of source control. 可以将这些值替换为自动生成的一部分,或使用本地部分类替代它们。You can replace these values as part of an automated build or override them using a local partial class. 你将在下一步中执行此操作。You will do this in the next step.

    仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 ApiKey 字段。The ApiKey field is only required if you chose to complete the Authenticate clients using an API Key section.

  13. 使用以下实现将另一个“空类”添加到 PushDemo 项目中,这一次,项目的名称为为 Config.local_secrets.cs 。Add another Empty Class to the PushDemo project this time called Config.local_secrets.cs with the following implementation.

    namespace PushDemo
    {
        public static partial class Config
        {
            static Config()
            {
                ApiKey = "<your_api_key>";
                BackendServiceEndpoint = "<your_api_app_url>";
            }
        }
    }
    

    备注

    将占位符值替换为你自己的值。Replace the placeholder values with your own. 应在构建后端服务时记下这些值。You should have made a note of these when you built the backend service. API 应用 URL 应为 https://<api_app_name>.chinacloudsites.cn/The API App URL should be https://<api_app_name>.chinacloudsites.cn/. 请务必将 *.local_secrets.* 添加到 gitignore 文件,以避免提交此文件。Remember to add *.local_secrets.* to your gitignore file to avoid committing this file.

    仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 ApiKey 字段。The ApiKey field is only required if you chose to complete the Authenticate clients using an API Key section.

  14. 使用以下实现将“空类”添加到名为 Bootstrap.cs 的 PushDemo 项目 。Add an Empty Class to the PushDemo project called Bootstrap.cs with the following implementation.

    using System;
    using PushDemo.Services;
    
    namespace PushDemo
    {
        public static class Bootstrap
        {
            public static void Begin(Func<IDeviceInstallationService> deviceInstallationService)
            {
                ServiceContainer.Register(deviceInstallationService);
    
                ServiceContainer.Register<IPushDemoNotificationActionService>(()
                    => new PushDemoNotificationActionService());
    
                ServiceContainer.Register<INotificationRegistrationService>(()
                    => new NotificationRegistrationService(
                        Config.BackendServiceEndpoint,
                        Config.ApiKey));
            }
        }
    }
    

    备注

    当应用启动时,每个平台都会调用 Begin 方法,并传入特定于平台的 IDeviceInstallationService 实现 。The Begin method will be called by each platform when the app launches passing in a platform-specific implementation of IDeviceInstallationService.

    仅当你选择完成使用 API 密钥对客户端进行身份验证部分时,才需要 NotificationRegistrationService apiKey 构造函数参数 。The NotificationRegistrationService apiKey constructor argument is only required if you chose to complete the Authenticate clients using an API Key section.

实现跨平台 UIImplement the cross-platform UI

  1. 在 PushDemo 项目中,打开 MainPage.xaml 并将 StackLayout 控件替换为以下项 。In the PushDemo project, open MainPage.xaml and replace the StackLayout control with the following.

    <StackLayout VerticalOptions="EndAndExpand"  
                 HorizontalOptions="FillAndExpand"
                 Padding="20,40">
        <Button x:Name="RegisterButton"
                Text="Register"
                Clicked="RegisterButtonClicked" />
        <Button x:Name="DeregisterButton"
                Text="Deregister"
                Clicked="DeregisterButtonClicked" />
    </StackLayout>
    
  2. 现在,在 MainPage.xaml.cs 中添加“只读”支持字段,以存储对 INotificationRegistrationService 实现的引用 。Now in MainPage.xaml.cs, add a readonly backing field to store a reference to the INotificationRegistrationService implementation.

    readonly INotificationRegistrationService _notificationRegistrationService;
    
  3. 在 MainPage 构造函数中,使用 ServiceContainer 解析 INotificationRegistrationService 实现,并将其分配给 notificationRegistrationService 支持字段 。In the MainPage constructor, resolve the INotificationRegistrationService implementation using the ServiceContainer and assign it to the notificationRegistrationService backing field.

    public MainPage()
    {
        InitializeComponent();
    
        _notificationRegistrationService =
            ServiceContainer.Resolve<INotificationRegistrationService>();
    }
    
  4. 调用相应的 Register/Deregister 方法,为 RegisterButton 和 DeregisterButton 按钮“已单击”事件实现事件处理程序 。Implement the event handlers for the RegisterButton and DeregisterButton buttons Clicked events calling the corresponding Register/Deregister methods.

    void RegisterButtonClicked(object sender, EventArgs e)
        => _notificationRegistrationService.RegisterDeviceAsync().ContinueWith((task)
            => { ShowAlert(task.IsFaulted ?
                    task.Exception.Message :
                    $"Device registered"); });
    
    void DeregisterButtonClicked(object sender, EventArgs e)
        => _notificationRegistrationService.DeregisterDeviceAsync().ContinueWith((task)
            => { ShowAlert(task.IsFaulted ?
                    task.Exception.Message :
                    $"Device deregistered"); });
    
    void ShowAlert(string message)
        => MainThread.BeginInvokeOnMainThread(()
            => DisplayAlert("PushDemo", message, "OK").ContinueWith((task)
                => { if (task.IsFaulted) throw task.Exception; }));
    
  5. 现在,在 App.xaml.cs 中,确保引用以下命名空间。Now in App.xaml.cs, ensure the following namespaces are referenced.

    using PushDemo.Models;
    using PushDemo.Services;
    using Xamarin.Essentials;
    using Xamarin.Forms;
    
  6. 为 IPushDemoNotificationActionService ActionTriggered 事件实现事件处理程序 。Implement the event handler for the IPushDemoNotificationActionService ActionTriggered event.

    void NotificationActionTriggered(object sender, PushDemoAction e)
        => ShowActionAlert(e);
    
    void ShowActionAlert(PushDemoAction action)
        => MainThread.BeginInvokeOnMainThread(()
            => MainPage?.DisplayAlert("PushDemo", $"{action} action received", "OK")
                .ContinueWith((task) => { if (task.IsFaulted) throw task.Exception; }));
    
  7. 在“应用”构造函数中,使用 ServiceContainer 解析 IPushNotificationActionService 实现,并订阅 IPushDemoNotificationActionService ActionTriggered 事件 。In the App constructor, resolve the IPushNotificationActionService implementation using the ServiceContainer and subscribe to the IPushDemoNotificationActionService ActionTriggered event.

    public App()
    {
        InitializeComponent();
    
        ServiceContainer.Resolve<IPushDemoNotificationActionService>()
            .ActionTriggered += NotificationActionTriggered;
    
        MainPage = new MainPage();
    }
    

    备注

    这只是为了演示推送通知操作的接收和传播。This is simply to demonstrate the receipt and propagation of push notification actions. 通常情况下,将以无提示方式处理这些操作(例如导航到特定视图或刷新某些数据),而不是通过根页面(在本例中为 MainPage)显示警报 。Typically, these would be handled silently for example navigating to a specific view or refreshing some data rather than displaying an alert via the root Page, MainPage in this case.

针对推送通知配置本地 iOS 项目Configure the native iOS project for push notifications

配置 Info.plist 和 Entitlements.plistConfigure Info.plist and Entitlements.plist

  1. 确保已登录到“Visual Studio” > “首选项...” > “发布” > “Apple 开发人员帐户”中的“Apple 开发人员帐户”,并且已下载相应“证书”和“预配配置文件” 。Ensure you've signed in to your Apple Developer Account in Visual Studio > Preferences... > Publishing > Apple Developer Accounts and the appropriate Certificate and Provisioning Profile has been downloaded. 应已在前面的步骤中创建这些资产。You should have created these assets as part of the previous steps.

  2. 在 PushDemo.iOS 中,打开 Info.plist,并确保 BundleIdentifier 与用于 Apple 开发人员门户中的各个预配配置文件的值匹配 。In PushDemo.iOS, open Info.plist and ensure that the BundleIdentifier matches the value that was used for the respective provisioning profile in the Apple Developer Portal. BundleIdentifier 的格式为 com.<organization>.PushDemoThe BundleIdentifier was in the format com.<organization>.PushDemo.

  3. 在同一文件中,将“最低系统版本”设置为 13.0 。In the same file, set Minimum system version to 13.0.

    备注

    在本教程中,仅支持运行 iOS 13.0 及更高版本的设备,但你可以将其扩展为支持运行较早版本的设备。Only those devices running iOS 13.0 and above are supported for the purposes of this tutorial however you can extend it to support devices running older versions.

  4. 为 PushDemo.iOS 打开“项目选项”(双击项目) 。Open the Project Options for PushDemo.iOS (double-click on the project).

  5. 在“项目选项”的“生成”>“iOS 捆绑签名”下,确保选中“团队”下的开发人员帐户 。In Project Options, under Build > iOS Bundle Signing, ensure that your Developer account is selected under Team. 然后,确保选中“自动管理签名”。这样就会自动选中签名证书和预配配置文件。Then, ensure "Automatically manage signing" is selected and your Signing Certificate and Provisioning Profile are automatically selected.

    备注

    如果未自动选择签名证书和预配配置文件,请选择“手动预配”,并单击“捆绑签名选项” 。If your Signing Certificate and Provisioning Profile have not been automatically selected, choose Manual Provisioning, then click on Bundle Signing Options. 确保为“签名标识”选择“团队”,并为“调试”和“发布”配置的预配配置文件选择特定于 PushDemo 的预配配置文件,同时确保在这两种情况下都为“平台”选择 iPhone 。Ensure that your Team is selected for Signing Identity and your PushDemo specific provisioning profile is selected for Provisioning Profile for both Debug and Release configurations ensuring that iPhone is selected for the Platform in both cases.

  6. 在 PushDemo.iOS 中,打开 Entitlements.plist,并确保“权利”选项卡中已选中“启用推送通知” 。然后,确保“源”选项卡中的“APS 环境”设置设为“开发” 。In PushDemo.iOS, open Entitlements.plist and ensure that Enable Push Notifications is checked when viewed in the Entitlements tab. Then, ensure the APS Environment setting is set to development when viewed in the Source tab.

处理 iOS 的推送通知Handle push notifications for iOS

  1. 控制 + 单击 PushDemo.iOS 项目,从“添加”菜单中选择“新建文件夹”,然后单击“添加”并使用“服务”作为“文件夹名称” 。Control + Click on the PushDemo.iOS project, choose New Folder from the Add menu, then click Add using Services as the Folder Name.

  2. 按住“Control”的同时单击“服务”文件夹,然后从“添加”菜单中选择“新建文件...” 。Control + Click on the Services folder, then choose New File... from the Add menu.

  3. 选择“常规” > “空类”,输入 DeviceInstallationService.cs 作为“名称”,然后单击“新建”以添加以下实现 。Select General > Empty Class, enter DeviceInstallationService.cs for the Name, then click New adding the following implementation.

    using System;
    using PushDemo.Models;
    using PushDemo.Services;
    using UIKit;
    
    namespace PushDemo.iOS.Services
    {
        public class DeviceInstallationService : IDeviceInstallationService
        {
            const int SupportedVersionMajor = 13;
            const int SupportedVersionMinor = 0;
    
            public string Token { get; set; }
    
            public bool NotificationsSupported
                => UIDevice.CurrentDevice.CheckSystemVersion(SupportedVersionMajor, SupportedVersionMinor);
    
            public string GetDeviceId()
                => UIDevice.CurrentDevice.IdentifierForVendor.ToString();
    
            public DeviceInstallation GetDeviceInstallation(params string[] tags)
            {
                if (!NotificationsSupported)
                    throw new Exception(GetNotificationsSupportError());
    
                if (string.isNullOrWhitespace(Token))
                    throw new Exception("Unable to resolve token for APNS");
    
                var installation = new DeviceInstallation
                {
                    InstallationId = GetDeviceId(),
                    Platform = "apns",
                    PushChannel = Token
                };
    
                installation.Tags.AddRange(tags);
    
                return installation;
            }
    
            string GetNotificationsSupportError()
            {
                if (!NotificationsSupported)
                    return $"This app only supports notifications on iOS {SupportedVersionMajor}.{SupportedVersionMinor} and above. You are running {UIDevice.CurrentDevice.SystemVersion}.";
    
                if (Token == null)
                    return $"This app can support notifications but you must enable this in your settings.";
    
    
                return "An error occurred preventing the use of push notifications";
            }
        }
    }
    

    备注

    此类提供一个唯一 ID(使用 UIDevice.IdentifierForVendor 值)和通知中心注册有效负载。This class provides a unique ID (using the UIDevice.IdentifierForVendor value) and the notification hub registration payload.

  4. 将新文件夹添加到名为“扩展”的 PushDemo.iOS 项目,然后使用以下实现将“空类”添加到名为 NSDataExtensions.cs 的文件夹。Add a new folder to the PushDemo.iOS project called Extensions then add an Empty Class to that folder called NSDataExtensions.cs with the following implementation.

    using System.Text;
    using Foundation;
    
    namespace PushDemo.iOS.Extensions
    {
        internal static class NSDataExtensions
        {
            internal static string ToHexString(this NSData data)
            {
                var bytes = data.ToArray();
    
                if (bytes == null)
                    return null;
    
                StringBuilder sb = new StringBuilder(bytes.Length * 2);
    
                foreach (byte b in bytes)
                    sb.AppendFormat("{0:x2}", b);
    
                return sb.ToString().ToUpperInvariant();
            }
        }
    }
    
  5. 在 AppDelegate.cs 中,确保已将以下命名空间添加到文件顶部。In AppDelegate.cs, ensure the following namespaces have been added to the top of the file.

    using System;
    using System.Diagnostics;
    using System.Threading.Tasks;
    using Foundation;
    using PushDemo.iOS.Extensions;
    using PushDemo.iOS.Services;
    using PushDemo.Services;
    using UIKit;
    using UserNotifications;
    using Xamarin.Essentials;
    
  6. 添加专用属性及其各自的支持字段,以存储对 IPushDemoNotificationActionService、INotificationRegistrationService 和 IDeviceInstallationService 实现的引用 。Add private properties and their respective backing fields to store a reference to the IPushDemoNotificationActionService, INotificationRegistrationService, and IDeviceInstallationService implementations.

    IPushDemoNotificationActionService _notificationActionService;
    INotificationRegistrationService _notificationRegistrationService;
    IDeviceInstallationService _deviceInstallationService;
    
    IPushDemoNotificationActionService NotificationActionService
        => _notificationActionService ??
            (_notificationActionService =
            ServiceContainer.Resolve<IPushDemoNotificationActionService>());
    
    INotificationRegistrationService NotificationRegistrationService
        => _notificationRegistrationService ??
            (_notificationRegistrationService =
            ServiceContainer.Resolve<INotificationRegistrationService>());
    
    IDeviceInstallationService DeviceInstallationService
        => _deviceInstallationService ??
            (_deviceInstallationService =
            ServiceContainer.Resolve<IDeviceInstallationService>());
    
  7. 添加 RegisterForRemoteNotifications 方法以注册用户通知设置,然后使用 APNS 注册远程通知 。Add the RegisterForRemoteNotifications method to register user notification settings and then for remote notifications with APNS.

    void RegisterForRemoteNotifications()
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            var pushSettings = UIUserNotificationSettings.GetSettingsForTypes(
                UIUserNotificationType.Alert |
                UIUserNotificationType.Badge |
                UIUserNotificationType.Sound,
                new NSSet());
    
            UIApplication.SharedApplication.RegisterUserNotificationSettings(pushSettings);
            UIApplication.SharedApplication.RegisterForRemoteNotifications();
        });
    }
    
  8. 添加 CompleteRegistrationAsync 方法以设置 IDeviceInstallationService.Token 属性值。Add the CompleteRegistrationAsync method to set the IDeviceInstallationService.Token property value. 刷新注册并缓存设备令牌(如果它自上次存储以来已发生更新)。Refresh the registration and cache the device token if it has been updated since it was last stored.

    Task CompleteRegistrationAsync(NSData deviceToken)
    {
        DeviceInstallationService.Token = deviceToken.ToHexString();
        return NotificationRegistrationService.RefreshRegistrationAsync();
    }
    
  9. 添加 ProcessNotificationActions 方法,用于处理 NSDictionary 通知数据并有条件地调用 NotificationActionService.TriggerAction 。Add the ProcessNotificationActions method for processing the NSDictionary notification data and conditionally calling NotificationActionService.TriggerAction.

    void ProcessNotificationActions(NSDictionary userInfo)
    {
        if (userInfo == null)
            return;
    
        try
        {
            var actionValue = userInfo.ObjectForKey(new NSString("action")) as NSString;
    
            if (!string.IsNullOrWhiteSpace(actionValue?.Description))
                NotificationActionService.TriggerAction(actionValue.Description);
        }
        catch (Exception ex)
        {
            Debug.WriteLine(ex.Message);
        }
    }
    
  10. 替代 RegisteredForRemoteNotifications 方法,该方法将 deviceToken 参数传递到 CompleteRegistrationAsync 方法 。Override the RegisteredForRemoteNotifications method passing the deviceToken argument to the CompleteRegistrationAsync method.

    public override void RegisteredForRemoteNotifications(
        UIApplication application,
        NSData deviceToken)
        => CompleteRegistrationAsync(deviceToken).ContinueWith((task)
            => { if (task.IsFaulted) throw task.Exception; });
    
  11. 替代 ReceivedRemoteNotification 方法,该方法将 userInfo 参数传递到 ProcessNotificationActions 方法 。Override the ReceivedRemoteNotification method passing the userInfo argument to the ProcessNotificationActions method.

    public override void ReceivedRemoteNotification(
        UIApplication application,
        NSDictionary userInfo)
        => ProcessNotificationActions(userInfo);
    
  12. 替代 FailedToRegisterForRemoteNotifications 方法以记录错误。Override the FailedToRegisterForRemoteNotifications method to log the error.

    public override void FailedToRegisterForRemoteNotifications(
        UIApplication application,
        NSError error)
        => Debug.WriteLine(error.Description);
    

    备注

    这就是一个占位符。This is very much a placeholder. 对于生产场景,需要实现正确的日志记录和错误处理。You will want to implement proper logging and error handling for production scenarios.

  13. 更新 FinishedLaunching 方法,以便在对 Forms.Init 的调用传入特定于平台的 IDeviceInstallationService 实现后立即调用 Bootstrap.BeginUpdate the FinishedLaunching method to call Bootstrap.Begin right after the call to Forms.Init passing in the platform-specific implementation of IDeviceInstallationService.

    Bootstrap.Begin(() => new DeviceInstallationService());
    
  14. 在同一方法中,有条件地请求授权,并在 Bootstrap.Begin 后立即注册远程通知。In the same method, conditionally request authorization and register for remote notifications immediately after Bootstrap.Begin.

    if (DeviceInstallationService.NotificationsSupported)
    {
        UNUserNotificationCenter.Current.RequestAuthorization(
                UNAuthorizationOptions.Alert |
                UNAuthorizationOptions.Badge |
                UNAuthorizationOptions.Sound,
                (approvalGranted, error) =>
                {
                    if (approvalGranted && error == null)
                        RegisterForRemoteNotifications();
                });
    }
    
  15. 仍然是在 FinishedLaunching 中,如果 options 参数包含可传入生成的 userInfo 对象的 UIApplication.LaunchOptionsRemoteNotificationKey,则在调用 LoadApplication 后立即调用 ProcessNotificationActions 。Still in FinishedLaunching, call ProcessNotificationActions immediately after the call to LoadApplication if the options argument contains the UIApplication.LaunchOptionsRemoteNotificationKey passing in the resulting userInfo object.

    using (var userInfo = options?.ObjectForKey(
        UIApplication.LaunchOptionsRemoteNotificationKey) as NSDictionary)
            ProcessNotificationActions(userInfo);
    

测试解决方案Test the solution

现在可以测试通过后端服务发送通知。You can now test sending notifications via the backend service.

发送测试通知Send a test notification

  1. Postman 中打开一个新选项卡。Open a new tab in Postman.

  2. 将请求设置为“POST”,然后输入以下地址:Set the request to POST, and enter the following address:

    https://<app_name>.chinacloudsites.cn/api/notifications/requests
    
  3. 如果选择完成使用 API 密钥对客户端进行身份验证部分,请确保将请求标头配置为包含 apikey 值。If you chose to complete the Authenticate clients using an API Key section, be sure to configure the request headers to include your apikey value.

    密钥Key Value
    apikeyapikey <your_api_key><your_api_key>
  4. 为“正文”选择“原始”选项,接下来从格式选项列表中选择“JSON”,然后包含一些占位符 JSON 内容 :Choose the raw option for the Body, then choose JSON from the list of format options, and then include some placeholder JSON content:

    {
        "text": "Message from Postman!",
        "action": "action_a"
    }
    
  5. 选择“代码”按钮,该按钮位于窗口右上角的“保存”按钮下方 。Select the Code button, which is under the Save button on the upper right of the window. 显示 HTML 请求时,请求应类似于以下示例(具体取决于是否包含 apikey 标头) :The request should look similar to the following example when displayed for HTML (depending on whether you included an apikey header):

    POST /api/notifications/requests HTTP/1.1
    Host: https://<app_name>.chinacloudsites.cn
    apikey: <your_api_key>
    Content-Type: application/json
    
    {
        "text": "Message from backend service",
        "action": "action_a"
    }
    
  6. 在一个或两个目标平台(Android 和 iOS)上运行 PushDemo 应用程序 。Run the PushDemo application on one or both of the target platforms (Android and iOS).

    备注

    如果你正在 Android 上进行测试,请确保未在“调试”模式中运行,或者如果通过运行应用程序部署了应用,请强制关闭该应用,然后从启动器重新启动 。If you are testing on Android ensure that you are not running in Debug, or if the app has been deployed by running the application then force close the app and start it again from the launcher.

  7. 在 PushDemo 应用中,点击“注册”按钮 。In the PushDemo app, tap on the Register button.

  8. 返回 Postman,关闭“生成代码片段”窗口(如果尚未关闭),然后单击“发送”按钮 。Back in Postman, close the Generate Code Snippets window (if you haven't done so already) then click the Send button.

  9. 验证是否在 Postman 中收到“200 OK”响应,并验证该警报是否显示在应用中,表明“已收到 ActionA 操作” 。Validate that you get a 200 OK response in Postman and the alert appears in the app showing ActionA action received.

  10. 关闭 PushDemo 应用,然后再次单击 Postman 中的“发送”按钮 。Close the PushDemo app, then click the Send button again in Postman.

  11. 验证是否在 Postman 中再次收到“200 OK”响应 。Validate that you get a 200 OK response in Postman again. 验证 PushDemo 应用的通知区域中是否显示带有正确消息的通知。Validate that a notification appears in the notification area for the PushDemo app with the correct message.

  12. 点击通知,确认它会打开应用并显示“已收到 ActionA 操作”警报。Tap on the notification to confirm that it opens the app and displayed the ActionA action received alert.

  13. 返回 Postman,修改之前的请求正文以发送无提示通知,指定“action”值为 action_b 而不是 action_a 。Back in Postman, modify the previous request body to send a silent notification specifying action_b instead of action_a for the action value.

    {
        "action": "action_b",
        "silent": true
    }
    
  14. 应用仍处于打开状态的情况下,单击 Postman 中的“发送”按钮 。With the app still open, click the Send button in Postman.

  15. 验证是否在 Postman 中收到“200 OK”响应,并验证是否在应用中显示该警报,表明“已收到 ActionB 操作”,而不是“已收到 ActionA 操作” 。Validate that you get a 200 OK response in Postman and that the alert appears in the app showing ActionB action received instead of ActionA action received.

  16. 关闭 PushDemo 应用,然后再次单击 Postman 中的“发送”按钮 。Close the PushDemo app, then click the Send button again in Postman.

  17. 验证是否在 Postman 中收到“200 OK”响应,并验证无提示通知是否未显示在通知区域中 。Validate that you get a 200 OK response in Postman and that the silent notification doesn't appear in the notification area.

后续步骤Next steps

现在应具有一个通过后端服务连接到通知中心的基本 Xamarin.Forms 应用,可以发送和接收通知。You should now have a basic Xamarin.Forms app connected to a notification hub via a backend service and can send and receive notifications.

可能需要调整本教程中使用的示例,使其适合你自己的方案。You'll likely need to adapt the example used in this tutorial to fit your own scenario. 此外,还建议实现更可靠的错误处理、重试逻辑和日志记录。Implementing more robust error handling, retry logic, and logging is also recommended.

Visual Studio App Center 可以快速合并到提供分析诊断的移动应用中,从而帮助你进行故障排除。Visual Studio App Center can be quickly incorporated into mobile apps providing analytics and diagnostics to aid in troubleshooting.

疑难解答Troubleshooting

后端服务没有响应No response from the backend service

在本地进行测试时,请确保后端服务正在运行,并且正在使用正确的端口。When testing locally, ensure that the backend service is running and is using the correct port.

如果要对 Azure API 应用进行测试,请检查服务是否正在运行、是否已部署并且已正确启动。If testing against the Azure API App, check the service is running and has been deployed and has started without error.

通过客户端进行测试时,请确保在 Postman 或移动应用配置中正确地指定了基址。Be sure to check you've specified the base address correctly in Postman or in the mobile app configuration when testing via the client. 在本地进行测试时,基址应显示为 https://<api_name>.chinacloudsites.cn/https://localhost:5001/The base address should indicatively be https://<api_name>.chinacloudsites.cn/ or https://localhost:5001/ when testing locally.

开始或停止调试会话后,Android 上没有收到通知Not receiving notifications on Android after starting or stopping a debug session

请确保在启动或停止调试会话后重新注册。Ensure you register again after starting or stopping a debug session. 调试器将导致生成新的 Firebase 令牌。The debugger will cause a new Firebase token to be generated. 还必须更新通知中心安装。The notification hub installation must be updated as well.

从后端服务收到 401 状态代码Receiving a 401 status code from the backend service

验证是否正在设置 apikey 请求标头,以及此值是否与你为后端服务配置的值相匹配。Validate that you're setting the apikey request header and this value matches the one you had configured for the backend service.

如果在本地进行测试时收到此错误,请确保在客户端配置中定义的密钥值与 API 使用的 Authentication:ApiKey 用户设置值相匹配。If you receive this error when testing locally, ensure the key value you defined in the client config, matches the Authentication:ApiKey user-setting value used by the API.

如果要对 API 应用进行测试,请确保客户端配置文件中的密钥值与你在 API 应用中使用的 Authentication:ApiKey 应用程序设置相匹配 。If you're testing with an API App, ensure the key value in the client config file matches the Authentication:ApiKey application setting you're using in the API App.

备注

如果在部署后端服务后创建或更改了此设置,则必须重启该服务才能使其生效。If you had created or changed this setting after you had deployed the backend service then you must restart the service in order for it take effect.

如果选择不完成使用 API 密钥对客户端进行身份验证部分,请确保未将 Authorize 特性应用到 NotificationsController 类 。If you chose not to complete the Authenticate clients using an API Key section, ensure that you didn't apply the Authorize attribute to the NotificationsController class.

从后端服务收到 404 状态代码Receiving a 404 status code from the backend service

验证终结点和 HTTP 请求方法是否正确。Validate that the endpoint and HTTP request method is correct. 例如,终结点应显示为:For example, the endpoints should indicatively be:

  • [PUT] https://<api_name>.chinacloudsites.cn/api/notifications/installations[PUT] https://<api_name>.chinacloudsites.cn/api/notifications/installations
  • [DELETE] https://<api_name>.chinacloudsites.cn/api/notifications/installations/<installation_id>[DELETE] https://<api_name>.chinacloudsites.cn/api/notifications/installations/<installation_id>
  • [POST] https://<api_name>.chinacloudsites.cn/api/notifications/requests[POST] https://<api_name>.chinacloudsites.cn/api/notifications/requests

如果在本地进行测试,应为:Or when testing locally:

  • [PUT] https://localhost:5001/api/notifications/installations[PUT] https://localhost:5001/api/notifications/installations
  • [DELETE] https://localhost:5001/api/notifications/installations/<installation_id>[DELETE] https://localhost:5001/api/notifications/installations/<installation_id>
  • [POST] https://localhost:5001/api/notifications/requests[POST] https://localhost:5001/api/notifications/requests

在客户端应用中指定基址时,请确保它以 / 结尾。When specifying the base address in the client app, ensure it ends with a /. 在本地进行测试时,基址应显示为 https://<api_name>.chinacloudsites.cn/https://localhost:5001/The base address should indicatively be https://<api_name>.chinacloudsites.cn/ or https://localhost:5001/ when testing locally.

无法注册并显示通知中心错误消息Unable to register and a notification hub error message is displayed

验证测试设备是否已连接到网络。Verify that the test device has network connectivity. 然后,通过设置断点来确定 Http 响应状态代码,以检查 HttpResponse 中的 StatusCode 属性值 。Then, determine the Http response status code by setting a breakpoint to inspect the StatusCode property value in the HttpResponse.

根据状态代码,查看以前的故障排除建议(如果适用)。Review the previous troubleshooting suggestions where applicable based on the status code.

在为相应的 API 返回这些特定状态代码的行上设置断点。Set a breakpoint on the lines that return these specific status codes for the respective API. 然后,在本地进行调试时,尝试调用后端服务。Then try calling the backend service when debugging locally.

使用适当的有效负载,通过 Postman 验证后端服务是否按预期方式工作。Validate the backend service is working as expected via Postman using the appropriate payload. 使用客户端代码为相关平台创建的实际有效负载。Use the actual payload created by the client code for the platform in question.

查看特定于平台的配置部分,确保没有遗漏任何步骤。Review the platform-specific configuration sections to ensure that no steps have been missed. 检查是否为相应平台的 installation idtoken 变量解析了合适的值。Check that suitable values are being resolved for installation id and token variables for the appropriate platform.

无法解析显示的设备错误消息的 IDUnable to resolve an ID for the device error message is displayed

查看特定于平台的配置部分,确保没有遗漏任何步骤。Review the platform-specific configuration sections to ensure that no steps have been missed.