教程:使用 Azure 通知中心通过后端服务向 .NET MAUI 应用发送推送通知

浏览示例。 浏览示例

推送通知将信息从后端系统传递到客户端应用。 Apple、Google 和其他平台都有自己的推送通知服务 (PNS)。 使用 Azure 通知中心可以跨平台集中通知,以便后端应用可以与负责向每个 PNS 分发通知的单个中心通信。

Azure 通知中心要求应用注册到中心,并根据需要定义模板和/或订阅标记:

  • 执行设备安装会将 PNS 句柄链接到 Azure 通知中心中的标识符。 有关注册的详细信息,请参阅注册管理
  • 设备通过模板来指定参数化消息模板。 可以根据设备自定义传入消息。 有关详细信息,请参阅通知中心模板
  • 标记可用于订阅消息类别,如新闻、体育和天气。 有关详细信息,请参阅路由和标记表达式

在本教程中,你将使用 Azure 通知中心向适用于 Android 和 iOS 的 .NET Multi-Platform App UI (.NET MAUI) 应用发送推送通知。 ASP.NET Core Web API 后端用于处理客户端的设备注册,并启动推送通知。 这些操作使用 Microsoft.Azure.NotificationHubs NuGet 包进行处理。 有关总体方法的详细信息,请参阅从后端管理注册

在本教程中,你将了解:

  • 设置推送通知服务和 Azure 通知中心。
  • 创建 ASP.NET Core Web API 后端应用。
  • 创建 .NET MAUI 应用。
  • 为 Android 应用配置推送通知。
  • 为 iOS 应用配置推送通知。
  • 测试应用。
  • 排查任何设置和配置问题。

先决条件

若要完成本教程,需要:

  • 具有活动订阅的 Azure 帐户。
  • 运行最新版本的 Visual Studio/Visual Studio Code 的电脑或 Mac,并安装 .NET Multi-Platform App UI 开发工作负载以及 ASP.NET 和 Web 开发工作负载。

对于 Android 而言,必须具有:

  • 开发人员解锁的物理设备或仿真器(运行安装了 Google Play Services 的 API 26 及更高版本)。

对于 iOS 而言,必须具有:

  • 一个有效的 Apple 开发人员帐户。
  • 一部运行 Xcode 的 Mac,以及一个安装到密钥链的有效开发人员证书。

然后,在 iOS 上,你应具有:

  • 在搭载 Apple Silicon 或 T2 处理器的 Mac 电脑上的 macOS 13 及更高版本中运行的 iOS 16 及更高版本模拟器。

    OR

  • 已注册到开发人员帐户的物理 iOS 设备(运行 iOS 13.0 及更高版本)。

  • 在 Apple 开发者帐户中注册并与证书关联的物理设备。

重要

在搭载 Apple Silicon 或 T2 处理器的 Mac 电脑上运行 macOS 13 及更高版本时,iOS 模拟器在 iOS 16 及更高版本中支持远程通知。 如果不满足这些硬件要求,则需要有效的 Apple 开发者帐户和物理设备。

若要遵循本教程,应熟悉以下内容:

虽然本教程面向 Visual Studio,但可以在电脑或 Mac 上使用 Visual Studio Code 进行操作。 然而,将存在一些需要调节的差异。 例如,用户界面和工作流的说明、模板名称和环境配置。

设置推送通知服务和 Azure 通知中心。

在本节中,将设置 Firebase Cloud MessagingApple Push Notification Services (APNS)。 然后,将创建并配置 Azure 通知中心来处理这些服务。

创建 Firebase 项目

若要创建 Firebase 项目,请执行以下操作:

  1. 在 Web 浏览器中,登录到 Firebase 控制台

  2. 在 Firebase 控制台中,选择“添加项目”按钮并创建新的 Firebase 项目,输入“PushDemo”作为“项目名称”

    注意

    将生成唯一名称。 默认情况下,此名称由提供的名称小写变体和用短划线分隔的生成数字组成。 如果需要可以对其进行更改,但前提是你的编辑仍全局唯一。

  3. 创建项目后,选择 Android 徽标,将 Firebase 添加到 Android 应用:

    在 Firebase Cloud Messaging 控制台中将 Firebase 添加到 Android 应用的屏幕截图。

  4. 在“将 Firebase 添加到 Android 应用”页面上,输入程序包的名称(也可以输入应用昵称),然后选择“注册应用”按钮

    将 Android 应用注册到 Firebase 的屏幕截图。

  5. 在“将 Firebase 添加到 Android 应用”页面上,选择“下载 google-services.json”按钮并将文件保存到本地文件夹,然后选择“下一步”按钮

    下载 google services JSON 文件的屏幕截图。

  6. 在“将 Firebase 添加到 Android 应用”页面上,选择“下一步”按钮

  7. 在“将 Firebase 添加到 Android 应用”页面上,选择“继续前往控制台”按钮

  8. 在 Firebase 控制台中,选择“项目概览”图标,然后选择“项目设置”

    在 Firebase Cloud Messaging 控制台中选择项目设置的屏幕截图。

  9. 在“项目设置”中,选择“Cloud Messaging”选项卡。你将看到已启用“Firebase Cloud Messaging API (V1)”

    确认已启用 Firebase Cloud Messaging V1 的屏幕截图。

  10. 在“项目设置”中,选择“服务帐户”选项卡,然后选择“生成新私钥”按钮

  11. 在“生成新私钥”对话框中,选择“生成密钥”按钮

    在 Firebase Cloud Messaging 控制台中生成新私钥的屏幕截图。

    此时将下载 JSON 文件,其中包含要输入到 Azure 通知中心的值。

为推送通知注册 iOS 应用

若要将推送通知发送到 iOS 应用,需要向 Apple 注册应用并注册推送通知。 可以通过执行以下 Azure 通知中心文档中的步骤来完成此操作:

如果要在物理设备上接收推送通知,则还需要创建预置描述文件

重要

若要在 iOS 上接收后台通知,必须向应用添加远程通知后台模式。 有关详细信息,请参阅 developer.apple.com 上的启用远程通知功能

创建 Azure 通知中心

若要在 Azure 门户中创建通知中心,请执行以下操作:

  1. 在 Web 浏览器中,登录到 Azure 门户
  2. 在 Azure 门户中,单击“创建资源”按钮,搜索并选择“通知中心”,然后选择“创建”按钮
  3. 在“通知中心”页面上,执行以下步骤:
    1. 在“订阅”字段中,选择要使用的 Azure 订阅的名称,然后选择现有资源组,或创建新的资源组

    2. 在“命名空间详细信息”字段中,输入新命名空间的唯一名称

    3. 在“通知中心详细信息”字段中,键入通知中心的名称。 名称是必填项,因为一个命名空间包含一个或多个通知中心。

    4. 在“位置”下拉列表中,选择要在其中创建通知中心的位置的值

    5. 查看“可用性区域”选项。 如果你选择了具有可用性区域的区域,则默认选中该复选框。

      注意

      可用性区域是一项付费功能,因此会向你的层级添加额外费用。

    6. 选择“灾难恢复”选项:“无”、“配对恢复区域”或“灵活的恢复区域”。 如果选择“配对恢复区域”,则会显示故障转移区域。 如果选择“灵活恢复区域”,请使用下拉列表从恢复区域列表中选择。

    7. 选择“创建”按钮。 此时将创建通知中心。

  4. 在 Azure 门户中,浏览到新创建的通知中心,然后前往“管理”>“访问策略”边栏选项卡
  5. 在“访问策略”边栏选项卡中,记下 DefaultFullSharedAccessSignature 策略的连接字符串。 稍后在生成与通知中心通信的后端服务时,将需要该字符串。

有关创建通知中心的详细信息,请参阅在 Azure 门户中创建 Azure 通知中心

在通知中心配置 Firebase Cloud Messaging

若要将通知中心配置为与 Firebase Cloud Messaging 通信,请执行以下操作:

  1. Azure 门户中,浏览到通知中心,然后选择“设置”>“Google (FCM v1)”边栏选项卡

  2. 在“Google (FCM v1)”边栏选项卡中,输入“私钥”、“客户端电子邮件”和“项目 ID”字段的值。 可以在从 Firebase Cloud Messaging 下载的私钥 JSON 文件中找到这些值:

    Azure 字段 JSON 键 JSON 值示例
    私钥 private_key 此值应以 -----BEGIN PRIVATE KEY-----\n 开头,以 -----END PRIVATE KEY-----\n 结尾。
    客户电子邮件 client_email firebase-adminsdk-55sfg@pushdemo-d6ab2.iam.gserviceaccount.com
    项目 ID project_id pushdemo-d6ab2
  3. 在“Google (FCM v1)”边栏选项卡中,选择“保存”按钮

在通知中心配置 Apple Push Notification 服务

Azure 门户中,浏览到通知中心,然后选择“设置”>“Apple (APNS)”边栏选项卡。 然后根据之前为通知中心创建证书时选择的方法执行相应的步骤。

重要

设置“应用程序模式”时,仅当希望将推送通知发送给从应用商店购买应用的用户时,才选择“生产”

选项 1 - 使用 .p12 推送证书

  1. 在“Apple (APNS)”边栏选项卡中,选择“证书”身份验证模式
  2. 在“Apple (APNS)”边栏选项卡中,选择“上传证书”字段旁边的文件图标。 然后,选择前面导出的 .p12 文件并上传该文件。
  3. 如果需要,在“Apple (APNS)”边栏选项卡的“密码”字段中输入证书密码
  4. 在“Apple (APNS)”边栏选项卡中,选择“沙盒”应用程序模式
  5. 在“Apple (APNS)”边栏选项卡中,选择“保存”按钮

选项 2 - 使用基于令牌的身份验证

  1. 在“Apple (APNS)”边栏选项卡中,选择“令牌”身份验证模式
  2. 在“Apple (APNS)”边栏选项卡中,输入之前获取的“密钥 ID”、“捆绑包 ID”、“团队 ID”和“令牌”字段的值。
  3. 在“Apple (APNS)”边栏选项卡中,选择“沙盒”应用程序模式
  4. 在“Apple (APNS)”边栏选项卡中,选择“保存”按钮

创建 ASP.NET Core Web API 后端应用

在本节中,将创建一个 ASP.NET Core Web API 后端,用于处理设备安装并向 .NET MAUI 应用发送通知。

创建 Web API 项目

若要创建 Web API 项目,请执行以下操作:

  1. 在 Visual Studio 中,创建 ASP.NET Core Web API 项目

    在 Visual Studio 中创建新的 ASP.NET Core Web API 项目的屏幕截图。

  2. 在“配置新项目”对话框中,将项目命名为 PushNotificationsAPI

  3. 在“其他信息”对话框中,确保启用“配置 HTTPS”和“使用控制器”复选框

    在 Visual Studio 中配置 ASP.NET Core Web API 项目的屏幕截图。

  4. 创建项目后,按 F5 以运行该项目。

    应用当前配置为使用 WeatherForecastController 作为 launchUrl,在 Properties\launchSettings.json 文件中进行设置。 应用将在 Web 浏览器中启动,并显示部分 JSON 数据。

    重要

    运行使用 HTTPS 的 ASP.NET Core 项目时,Visual Studio 将检测是否已将 ASP.NET Core HTTPS 开发证书安装到本地用户证书存储,如果缺少该证书,Visual Studio 将建议安装并信任证书。

  5. 关闭 Web 浏览器。

  6. 在“解决方案资源管理器”中,展开“Controllers”文件夹,并删除 WeatherForecastController.cs。

  7. 在“解决方案资源管理器”中,在项目的根目录中删除 WeatherForecast.cs

  8. 打开命令窗口,然后导航到包含项目文件的目录。 然后运行下列命令:

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

    将占位符值替换为自己的 Azure 通知中心名称和连接字符串值。 可以在 Azure 通知中心的以下位置找到这些值:

    配置值 位置
    NotificationHub:Name 请参阅“概览”页面顶部“基础”摘要中的“名称”
    NotificationHub:ConnectinString 请参阅“访问策略”页面上的 DefaultFullSharedAccessSignature*

    使用机密管理器工具设置本地配置值。 这样,可以将 Azure 通知中心机密与 Visual Studio 解决方案分离,以确保它们最终不会出现在源代码管理中。

    提示

    对于生产方案,请注意 Azure KeyVault 等服务,以安全地存储连接字符串。

使用 API 密钥对客户端进行身份验证

若要使用 API 密钥对客户端进行身份验证,请执行以下操作:

  1. 打开命令窗口,然后导航到包含项目文件的目录。 然后运行下列命令:

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

    将占位符值替换为 API 密钥,可以是任何值。

  2. 在 Visual Studio 中,将名为“Authentication”的新文件夹添加到项目中,然后将名为 ApiKeyAuthOptions 的新类添加到“Authentication”文件夹中,并将其代码替换为以下代码

    using Microsoft.AspNetCore.Authentication;
    
    namespace PushNotificationsAPI.Authentication;
    
    public class ApiKeyAuthOptions : AuthenticationSchemeOptions
    {
        public const string DefaultScheme = "ApiKey";
        public string Scheme => DefaultScheme;
        public string ApiKey { get; set; }
    }
    
  3. 在 Visual Studio 中,将名为 ApiKeyAuthHandler 的新类添加到“Authentication”文件夹中,并将其代码替换为以下代码

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.Extensions.Options;
    using System.Security.Claims;
    using System.Text.Encodings.Web;
    
    namespace PushNotificationsAPI.Authentication;
    
    public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOptions>
    {
        const string ApiKeyIdentifier = "apikey";
    
        public ApiKeyAuthHandler(
            IOptionsMonitor<ApiKeyAuthOptions> options,
            ILoggerFactory logger,
            UrlEncoder encoder)
            : base(options, logger, encoder)
        {
        }
    
        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 密钥方案。

  4. 在 Visual Studio 中,将名为 AuthenticationBuilderExtensions 的新类添加到“Authentication”文件夹中,并将其代码替换为以下代码

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

    此扩展方法将用于简化 Program.cs 中的中间件配置代码

  5. 在 Visual Studio 中,打开 Program.cs 并更新代码,以在调用 builder.Services.AddControllers 方法下方配置 API 密钥身份验证

    using PushNotificationsAPI.Authentication;
    
    builder.Services.AddControllers();
    
    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
        options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
    }).AddApiKeyAuth(builder.Configuration.GetSection("Authentication").Bind);
    
  6. 在 Program.cs 中,更新 // Configure the HTTP request pipeline 注释下方的代码以调用 UseRoutingUseAuthenticationMapControllers 扩展方法

    // Configure the HTTP request pipeline.
    
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();
    
    app.Run();
    

    UseAuthentication 扩展方法注册使用之前已注册的身份验证方案的中间件。 必须在依赖于要进行身份验证的用户的任何中间件之前,调用 UseAuthentication

    注意

    虽然 API 密钥的安全性低于令牌,但足以满足本教程的需求,并且可以通过 ASP.NET 中间件轻松配置。

添加和配置服务

若要在 Web API 后端应用中添加和配置服务,请执行以下操作:

  1. 在 Visual Studio 中,将 Microsoft.Azure.NotificationHubs NuGet 包添加到项目中。 此 NuGet 包用于访问在服务中封装的通知中心。

  2. 在 Visual Studio 中,将名为“Models”的新文件夹添加到项目中,然后将名为 PushTemplates 的新类添加到“Models”文件夹中,并将其代码替换为以下代码

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

    PushTemplates 类包含通用和无提示推送通知的标记化通知有效负载。 这些有效负载在安装外定义,以便无需通过服务更新现有安装即可进行试验。 以这种方式处理对安装的更改超出了本文的范围。 在产品方案中,请考虑使用自定义模板

  3. 在 Visual Studio 中,将名为 DeviceInstallation 的新类添加到“Models”文件夹中,并将其代码替换为以下代码

    using System.ComponentModel.DataAnnotations;
    
    namespace PushNotificationsAPI.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>();
    }
    
  4. 在 Visual Studio 中,将名为 NotificationRequest 的新类添加到“Models”文件夹中,并将其代码替换为以下代码

    namespace PushNotificationsAPI.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; }
    }
    
  5. 在 Visual Studio 中,将名为 NotificationHubOptions 的新类添加到“Models”文件夹中,并将其代码替换为以下代码

    using System.ComponentModel.DataAnnotations;
    
    namespace PushNotificationsAPI.Models;
    
    public class NotificationHubOptions
    {
        [Required]
        public string Name { get; set; }
    
        [Required]
        public string ConnectionString { get; set; }
    }
    
  6. 在 Visual Studio 中,将名为“Services”的新文件夹添加到项目中,然后将名为 INotificationService 的新接口添加到“Services”文件夹中,并将其代码替换为以下代码

    using PushNotificationsAPI.Models;
    
    namespace PushNotificationsAPI.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);
    }
    
  7. 在 Visual Studio 中,将名为 NotificationHubService 的新类添加到“Services”文件夹中,并将其代码替换为以下代码

    using Microsoft.Extensions.Options;
    using Microsoft.Azure.NotificationHubs;
    using PushNotificationsAPI.Models;
    
    namespace PushNotificationsAPI.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.FcmV1).ToLower(), NotificationPlatform.FcmV1 }
            };
        }
    
        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.SendFcmV1NativeNotificationAsync(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.SendFcmV1NativeNotificationAsync(androidPayload, tags, token),
                _hub.SendAppleNativeNotificationAsync(iOSPayload, tags, token)
            };
    
            return Task.WhenAll(sendTasks);
        }
    }
    

    提供给 SendTemplateNotificationsAsync 方法的标记表达式限制为 20 个标记(如果它们仅包含 OR)。 否则,它们限制为 6 个标记。 有关详细信息,请参阅路由和标记表达式

  8. 在 Visual Studio 中,打开 Program.cs 并更新代码,以在调用 builder.Services.AddAuthentication 方法下方添加 NotificationHubService 作为 INotificationService 的单一实例实现

    using PushNotificationsAPI.Authentication;
    using PushNotificationsAPI.Services;
    using PushNotificationsAPI.Models;
    
    var builder = WebApplication.CreateBuilder(args);
    
    // Add services to the container.
    
    builder.Services.AddControllers();
    
    builder.Services.AddAuthentication(options =>
    {
        options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
        options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
    }).AddApiKeyAuth(builder.Configuration.GetSection("Authentication").Bind);
    
    builder.Services.AddSingleton<INotificationService, NotificationHubService>();
    builder.Services.AddOptions<NotificationHubOptions>()
        .Configure(builder.Configuration.GetSection("NotificationHub").Bind)
        .ValidateDataAnnotations();
    
    var app = builder.Build();
    

创建通知 REST API

若要创建通知 REST API,请执行以下操作:

  1. 在 Visual Studio 中,将名为 NotificationsController 的新控制器添加到“Controllers”文件夹中

    提示

    选择“具有读/写操作的 API 控制器”模板

  2. 在 NotificationsController.cs 文件的顶部添加以下 using 语句

    using System.ComponentModel.DataAnnotations;
    using System.Net;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    using PushNotificationsAPI.Models;
    using PushNotificationsAPI.Services;
    
  3. 在 NotificationsController.cs 文件中,将 Authorize 属性添加到 NotificationsController

    [Authorize]
    [ApiController]
    [Route("api/[controller]")]
    public class NotificationsController : ControllerBase
    
  4. 在 NotificationsController.cs 文件中,更新 NotificationsContoller 构造函数以接受 INotificationService 的注册实例作为参数,并将其分配给只读成员

    readonly INotificationService _notificationService;
    
    public NotificationsController(INotificationService notificationService)
    {
        _notificationService = notificationService;
    }
    
  5. 在 NotificationsContoller.cs 文件中,将所有方法替换为以下代码

    [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)
    {
        // Probably want to ensure deletion even if the connection is broken
        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();
    }
    
  6. 在 Properties/launchSettings.json 文件中,将每个配置文件的 launchUrl 属性从 weatherforecast 更改为 api/notifications

创建 API 应用

现在,在 Azure 应用服务中创建 API 应用,以便托管后端服务。 可以直接从 Visual Studio 或 Visual Studio Code、使用 Azure CLI、Azure PowerShell、Azure Developer CLI 以及通过 Azure 门户完成此操作。 有关详细信息,请参阅发布 Web 应用

若要在 Azure 门户中创建 API 应用,请执行以下操作:

  1. 在 Web 浏览器中,登录到 Azure 门户

  2. 在 Azure 门户中,单击“创建资源”按钮,搜索并选择“API 应用”,然后选择“创建”按钮

  3. 在“创建 API 应用”页面中,更新以下字段,然后再选择“创建”按钮:

    字段 操作
    订阅 选择在其中创建了通知中心的同一目标订阅。
    资源组 选中在其中创建了通知中心的同一资源组。
    名称 选择全局唯一名称。
    运行时堆栈 确保已选择最新版本的 .NET。
  4. 预配 API 应用后,导航到该资源

  5. 在“概览”页面上,记下默认域值。 此 URL 是后端终结点,将由 .NET MAUI 应用使用。 URL 将使用指定的 API 应用名称,其格式为 https://<app_name>.azurewebsites.net

  6. 在 Azure 门户中,浏览到“设置”>“环境变量”边栏选项卡,确保选中“应用设置”选项卡。 然后使用“添加”按钮添加以下设置

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

    重要

    为简单起见,添加了 Authentication:ApiKey 应用程序设置。 对于生产方案,请注意 Azure KeyVault 等服务,以安全地存储连接字符串。

    输入上述所有设置后,选择“应用”按钮,然后选择“确认”按钮

发布后端服务

若要将后端服务发布到 Azure,请执行以下操作:

  1. 在 Visual Studio 中,右键单击项目并选择“发布”
  2. 在“发布”向导中,选择“Azure”,然后选择“下一步”按钮
  3. 在“发布”向导中,选择“Azure 应用服务(Windows)”,然后选择“下一步”按钮
  4. 在“发布”向导中,按照身份验证流将 Visual Studio 连接到 Azure 订阅并发布应用。

Visual Studio 将生成、打包应用并将其发布到 Azure,然后在默认浏览器中启动该应用。 有关详细信息,请参阅发布 ASP.NET Web 应用

提示

可以从 Azure 门户中 API 应用的“概览”边栏选项卡中下载应用的发布配置文件,然后使用 Visual Studio 中的配置文件发布应用

验证已发布的 API

若要检查 API 应用是否已正确发布,应使用所选的 REST 工具将 POST 请求发送到以下地址:

https://<app_name>.azurewebsites.net/api/notifications/requests

注意

基址为 https://<app_name>.azurewebsites.net

确保将请求标头配置为包含键 apikey 及其值,将正文设置为 raw,并使用以下占位符 JSON 内容:

{}

你应该会从服务收到 400 Bad Request 响应。

注意

尚无法使用有效的请求数据测试 API,因为此操作需要来自 .NET MAUI 应用的特定于平台的信息。

有关调用 REST API 的详细信息,请参阅在 .NET MAUI 中使用 .http 文件使用 Http Repl 测试 Web API。 在 Visual Studio Code 中,REST 客户端可用于测试 REST API。

创建 .NET MAUI 应用

在本节中,将生成一个 .NET Multi-Platform App UI (.NET MAUI) 应用,使你能够注册以通过后端服务从通知中心接收推送通知,还可以取消注册。

若要创建 .NET MAUI 应用,请执行以下操作:

  1. 在 Visual Studio 中,使用“.NET MAUI 应用”项目模板创建一个名为 PushNotificationsDemo 的新 .NET MAUI 应用

  2. 在 Visual Studio 中,将名为“Models”的新文件夹添加到 .NET MAUI 项目中,然后将名为 DeviceInstallation 的新类添加到“Models”文件夹中,并将其代码替换为以下代码

    using System.Text.Json.Serialization;
    
    namespace PushNotificationsDemo.Models;
    
    public class DeviceInstallation
    {
        [JsonPropertyName("installationId")]
        public string InstallationId { get; set; }
    
        [JsonPropertyName("platform")]
        public string Platform { get; set; }
    
        [JsonPropertyName("pushChannel")]
        public string PushChannel { get; set; }
    
        [JsonPropertyName("tags")]
        public List<string> Tags { get; set; } = new List<string>();
    }
    
  3. 在 Visual Studio 中,将名为 PushDemoAction 的枚举添加到“Models”文件夹中,并将其代码替换为以下代码

    namespace PushNotificationsDemo.Models;
    
    public enum PushDemoAction
    {
        ActionA,
        ActionB
    }
    
  4. 在 Visual Studio 中,将名为“Services”的新文件夹添加到 .NET MAUI 项目中,然后将名为 IDeviceInstallationService 的新接口添加到“Services”文件夹中,并将其代码替换为以下代码

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

    稍后将在每个平台上实现此接口,以提供后端服务所需的 DeviceInstallation 信息。

  5. 在 Visual Studio 中,将名为 INotificationRegistrationService 的接口添加到“Services”文件夹中,并将其代码替换为以下代码

    namespace PushNotificationsDemo.Services;
    
    public interface INotificationRegistrationService
    {
        Task DeregisterDeviceAsync();
        Task RegisterDeviceAsync(params string[] tags);
        Task RefreshRegistrationAsync();
    }
    

    此接口将处理客户端和后端服务之间的交互。

  6. 在 Visual Studio 中,将名为 INotificationActionService 的接口添加到“Services”文件夹中,并将其代码替换为以下代码

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

    此接口将用作集中处理通知操作的简单机制。

  7. 在 Visual Studio 中,将名为 IPushDemoNotificationActionService 的接口添加到“Services”文件夹中,并将其代码替换为以下代码

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

    IPushDemoNotificationActionService 类型特定于此应用,并使用 PushDemoAction 枚举来识别使用强类型方法触发的操作。

  8. 在 Visual Studio 中,将名为 NotificationRegistrationService 的类添加到“Services”文件夹中,并将其代码替换为以下代码

    using System.Text;
    using System.Text.Json;
    using PushNotificationsDemo.Models;
    
    namespace PushNotificationsDemo.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;
    
        IDeviceInstallationService DeviceInstallationService =>
            _deviceInstallationService ?? (_deviceInstallationService = Application.Current.MainPage.Handler.MauiContext.Services.GetService<IDeviceInstallationService>());
    
        public NotificationRegistrationService(string baseApiUri, string apiKey)
        {
            _client = new HttpClient();
            _client.DefaultRequestHeaders.Add("Accept", "application/json");
            _client.DefaultRequestHeaders.Add("apikey", apiKey);
    
            _baseApiUrl = baseApiUri;
        }
    
        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, JsonSerializer.Serialize(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 = JsonSerializer.Deserialize<string[]>(serializedTags);
    
            await RegisterDeviceAsync(tags);
        }
    
        async Task SendAsync<T>(HttpMethod requestType, string requestUri, T obj)
        {
            string serializedContent = null;
    
            await Task.Run(() => serializedContent = JsonSerializer.Serialize(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();
        }
    }
    
  9. 在 Visual Studio 中,将名为 PushDemoNotificationActionService 的类添加到“Services”文件夹中,并将其代码替换为以下代码

    using PushNotificationsDemo.Models;
    
    namespace PushNotificationsDemo.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);
        }
    }
    
  10. 在 Visual Studio 中,将名为 Config 的类添加到项目的根目录中,并将其代码替换为以下代码:

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

    Config 类是将机密从源代码管理中排除的简单方法。 可以将这些值替换为自动生成的一部分,或使用本地部分类替代它们。

    重要

    在 .NET MAUI 应用中指定基址时,请确保它以 / 结尾。

  11. 在 Visual Studio 中,将名为 Config.local_secrets 的类添加到项目的根目录中。 然后,将 Config.local_secrets.cs 文件中的代码替换为以下代码

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

    将占位符值替换为创建后端服务时选择的值。 BackendServiceEndpoint URL 应使用 https://<api_app_name>.azurewebsites.net/ 格式。

    提示

    请务必将 *.local_secrets.* 添加到 .gitignore 文件,以避免将此文件提交到源代码管理。

创建 UI

若要创建应用的 UI,请执行以下操作:

  1. 在 Visual Studio 中,打开 MainPage.xaml,并将 VerticalStackLayout 及其子项替换为以下 XAML

    <VerticalStackLayout Margin="20"
                         Spacing="6">
        <Button x:Name="registerButton"
                Text="Register"
                Clicked="OnRegisterButtonClicked" />
        <Button x:Name="deregisterButton"
                Text="Deregister"
                Clicked="OnDeregisterButtonClicked" />
    </VerticalStackLayout>
    
  2. 在 Visual Studio 中,打开 MainPage.xaml.cs 并为 PushNotificationsDemo.Services 命名空间添加 using 语句

    using PushNotificationsDemo.Services;
    
  3. 在 MainPage.xaml.cs 中,添加 readonly 支持字段以存储对 INotificationRegistrationService 实现的引用

    readonly INotificationRegistrationService _notificationRegistrationService;
    
  4. MainPage 构造函数中,解析 INotificationRegistrationService 实现并将其分配给 _notificationRegistrationService 支持字段:

    public MainPage(INotificationRegistrationService service)
    {
        InitializeComponent();
    
        _notificationRegistrationService = service;
    }
    
  5. MainPage 类中,实现 OnRegisterButtonClickedOnDeregisterButtonClicked 事件处理程序,针对 INotificationRegistrationService 对象调用相应的注册和取消注册方法:

    void OnRegisterButtonClicked(object sender, EventArgs e)
    {
        _notificationRegistrationService.RegisterDeviceAsync()
            .ContinueWith((task) =>
            {
                ShowAlert(task.IsFaulted ? task.Exception.Message : $"Device registered");
            });
    }
    
    void OnDeregisterButtonClicked(object sender, EventArgs e)
    {
        _notificationRegistrationService.DeregisterDeviceAsync()
            .ContinueWith((task) =>
            {
                ShowAlert(task.IsFaulted ? task.Exception.Message : $"Device deregistered");
            });
    }
    
    void ShowAlert(string message)
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            DisplayAlert("Push notifications demo", message, "OK")
                .ContinueWith((task) =>
                {
                    if (task.IsFaulted)
                        throw task.Exception;
                });
        });
    }
    

    重要

    在应用中,注册和取消注册是在响应用户输入时执行的,以便更轻松地探索和测试此功能。 在生产应用中,通常会在应用生命周期的适当时间点执行注册和取消注册操作,而无需显式用户输入。

  6. 在 Visual Studio 中,打开 App.xaml.cs 并添加以下 using 语句

    using PushNotificationsDemo.Models;
    using PushNotificationsDemo.Services;
    
  7. 在 App.xaml.cs 中,添加 readonly 支持字段以存储对 IPushDemoNotificationActionService 实现的引用

    readonly IPushDemoNotificationActionService _actionService;
    
  8. App 构造函数中,解析 IPushDemoNotificationActionService 实现并将其分配给 _actionService 支持字段,并订阅 IPushDemoNotificationActionService.ActionTriggered 事件:

    public App(IPushDemoNotificationActionService service)
    {
        InitializeComponent();
    
        _actionService = service;
        _actionService.ActionTriggered += NotificationActionTriggered;
    
        MainPage = new AppShell();
    }
    
  9. App 类中,实现 IPushDemoNotificationActionService.ActionTriggered 事件的事件处理程序:

    void NotificationActionTriggered(object sender, PushDemoAction e)
    {
        ShowActionAlert(e);
    }
    
    void ShowActionAlert(PushDemoAction action)
    {
        MainThread.BeginInvokeOnMainThread(() =>
        {
            MainPage?.DisplayAlert("Push notifications demo", $"{action} action received.", "OK")
                .ContinueWith((task) =>
                {
                    if (task.IsFaulted)
                        throw task.Exception;
                });
        });
    }
    

    ActionTriggered 事件的事件处理程序演示推送通知操作的接收和传播。 通常情况下,将以无提示方式处理这些操作(例如导航到特定视图或刷新某些数据),而不是显示警报。

配置 Android 应用

若要在 Android 上配置 .NET MAUI 应用以接收和处理推送通知,请执行以下操作:

  1. 在 Visual Studio 中,将 Xamarin.Firebase.Messaging NuGet 包添加到 .NET MAUI 应用项目中。

  2. 在 Visual Studio 中,将 google-services.json 文件添加到 .NET MAUI 应用项目的“Platforms/Android”文件夹中。 将文件添加到项目后,应该会添加 GoogleServicesJson 的生成操作:

    <ItemGroup Condition="'$(TargetFramework)' == 'net8.0-android'">
      <GoogleServicesJson Include="Platforms\Android\google-services.json" />
    </ItemGroup>
    

    提示

    请务必将 google-services.json 添加到 .gitignore 文件,以避免将此文件提交到源代码管理。

  3. 在 Visual Studio 中,编辑项目文件 (*.csproj),并将 Android 的 SupportedOSPlatformVersion 设置为 26.0:

    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">26.0</SupportedOSPlatformVersion>
    

    Google 在 API 26 中对 Android 通知渠道进行了更改。 有关详细信息,请参阅 developer.android.com 上的通知渠道

  4. 在项目的“Platforms/Android”文件夹中,添加名为 DeviceInstallationService 的新类,并将其代码替换为以下代码:

    using Android.Gms.Common;
    using PushNotificationsDemo.Models;
    using PushNotificationsDemo.Services;
    using static Android.Provider.Settings;
    
    namespace PushNotificationsDemo.Platforms.Android;
    
    public class DeviceInstallationService : IDeviceInstallationService
    {
        public string Token { get; set; }
    
        public bool NotificationsSupported
            => GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(Platform.AppContext) == ConnectionResult.Success;
    
        public string GetDeviceId()
            => Secure.GetString(Platform.AppContext.ContentResolver, Secure.AndroidId);
    
        public DeviceInstallation GetDeviceInstallation(params string[] tags)
        {
            if (!NotificationsSupported)
                throw new Exception(GetPlayServicesError());
    
            if (string.IsNullOrWhiteSpace(Token))
                throw new Exception("Unable to resolve token for FCMv1.");
    
            var installation = new DeviceInstallation
            {
                InstallationId = GetDeviceId(),
                Platform = "fcmv1",
                PushChannel = Token
            };
    
            installation.Tags.AddRange(tags);
    
            return installation;
        }
    
        string GetPlayServicesError()
        {
            int resultCode = GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(Platform.AppContext);
    
            if (resultCode != ConnectionResult.Success)
                return GoogleApiAvailability.Instance.IsUserResolvableError(resultCode) ?
                           GoogleApiAvailability.Instance.GetErrorString(resultCode) :
                           "This device isn't supported.";
    
            return "An error occurred preventing the use of push notifications.";
        }
    }
    

    此类提供一个唯一 ID(使用 Secure.AndroidId 值)和通知中心注册有效负载。

  5. 在项目的“Platforms/Android”文件夹中,添加名为 PushNotificationFirebaseMessagingService 的新类,并将其代码替换为以下代码:

    using Android.App;
    using Firebase.Messaging;
    using PushNotificationsDemo.Services;
    
    namespace PushNotificationsDemo.Platforms.Android;
    
    [Service(Exported = false)]
    [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
    public class PushNotificationFirebaseMessagingService : FirebaseMessagingService
    {
        IPushDemoNotificationActionService _notificationActionService;
        INotificationRegistrationService _notificationRegistrationService;
        IDeviceInstallationService _deviceInstallationService;
        int _messageId;
    
        IPushDemoNotificationActionService NotificationActionService =>
            _notificationActionService ?? (_notificationActionService = IPlatformApplication.Current.Services.GetService<IPushDemoNotificationActionService>());
    
        INotificationRegistrationService NotificationRegistrationService =>
            _notificationRegistrationService ?? (_notificationRegistrationService = IPlatformApplication.Current.Services.GetService<INotificationRegistrationService>());
    
        IDeviceInstallationService DeviceInstallationService =>
            _deviceInstallationService ?? (_deviceInstallationService = IPlatformApplication.Current.Services.GetService<IDeviceInstallationService>());
    
        public override void OnNewToken(string token)
        {
            DeviceInstallationService.Token = token;
    
            NotificationRegistrationService.RefreshRegistrationAsync()
                .ContinueWith((task) =>
                {
                    if (task.IsFaulted)
                        throw task.Exception;
                });
        }
    
        public override void OnMessageReceived(RemoteMessage message)
        {
            base.OnMessageReceived(message);
    
            if (message.Data.TryGetValue("action", out var messageAction))
                NotificationActionService.TriggerAction(messageAction);
        }
    }
    

    此类具有包含 com.google.firebase.MESSAGING_EVENT 筛选器的 IntentFilter 属性。 此筛选器使 Android 能够将传入消息传递到此类进行处理。

    有关 Firebase Cloud Messaging 消息格式的信息,请参阅 developer.android.com 上的 FCM 消息

  6. 在 Visual Studio 中,打开“Platforms/Android”文件夹中的 MainActivity.cs 文件,并添加以下 using 语句

    using Android.App;
    using Android.Content;
    using Android.Content.PM;
    using Android.OS;
    using PushNotificationsDemo.Services;
    using Firebase.Messaging;
    
  7. MainActivity 类中,将 LaunchMode 设置为 SingleTop,以便在打开时不会再次创建 MainActivity

    [Activity(
        Theme = "@style/Maui.SplashTheme",
        MainLauncher = true,
        LaunchMode = LaunchMode.SingleTop,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
    
  8. MainActivity 类中,添加支持字段来存储对 IPushDemoNotificationActionServiceIDeviceInstallationService 实现的引用:

    IPushDemoNotificationActionService _notificationActionService;
    IDeviceInstallationService _deviceInstallationService;
    
  9. MainActivity 类中,添加 NotificationActionServiceDeviceInstallationService 私有属性,以便从应用的依赖项注入容器中检索其具体实现:

    IPushDemoNotificationActionService NotificationActionService =>
        _notificationActionService ?? (_notificationActionService = IPlatformApplication.Current.Services.GetService<IPushDemoNotificationActionService>());
    
    IDeviceInstallationService DeviceInstallationService =>
        _deviceInstallationService ?? (_deviceInstallationService = IPlatformApplication.Current.Services.GetService<IDeviceInstallationService>());
    
  10. MainActivity 类中,实现 Android.Gms.Tasks.IOnSuccessListener 接口以检索和存储 Firebase 令牌:

    public class MainActivity : MauiAppCompatActivity, Android.Gms.Tasks.IOnSuccessListener
    {
        public void OnSuccess(Java.Lang.Object result)
        {
            DeviceInstallationService.Token = result.ToString();
        }
    }
    
  11. MainActivity 类中,添加 ProcessNotificationActions 方法,该方法将检查给定 Intent 是否具有名为 action 的额外值,然后使用 IPushDemoNotificationActionService 实现有条件地触发 action

    void ProcessNotificationsAction(Intent intent)
    {
        try
        {
            if (intent?.HasExtra("action") == true)
            {
                var action = intent.GetStringExtra("action");
    
                if (!string.IsNullOrEmpty(action))
                    NotificationActionService.TriggerAction(action);
            }
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex.Message);
        }
    }
    
  12. MainActivity 类中,替代 OnNewIntent 方法以调用 ProcessNotificationActions 方法:

    protected override void OnNewIntent(Intent? intent)
    {
        base.OnNewIntent(intent);
        ProcessNotificationsAction(intent);
    }
    

    由于 ActivityLaunchMode 设置为 SingleTop,因此 Intent 将通过 OnNewIntent 替代(而不是 OnCreate 方法)发送到现有 Activity 实例。 因此,必须在 OnNewIntentOnCreate 中处理传入意向。

  13. MainActivity 类中,替代 OnCreate 方法以调用 ProcessNotificationActions 方法,并从 Firebase 检索令牌,将 MainActivity 添加为 IOnSuccessListener

    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);
    
        if (DeviceInstallationService.NotificationsSupported)
            FirebaseMessaging.Instance.GetToken().AddOnSuccessListener(this);
    
        ProcessNotificationsAction(Intent);
    }
    

    注意

    必须在每次运行应用时重新注册该应用,并从调试会话中停止该应用以继续接收推送通知。

  14. 在 Visual Studio 中,将 POST_NOTIFICATIONS 权限添加到“Platforms/Android”文件夹中的 AndroidManifest.xml 文件

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    

    有关此权限的详细信息,请参阅 developer.android.com 上的通知运行时权限

  15. 在 Visual Studio 中,打开 MainPage.xaml.cs 并将以下代码添加到 MainPage

    #if ANDROID
            protected override async void OnAppearing()
            {
                base.OnAppearing();
    
                PermissionStatus status = await Permissions.RequestAsync<Permissions.PostNotifications>();
            }
    #endif
    

    出现 MainPage 时,将在 Android 上运行此代码,并请求用户授予 POST_NOTIFICATIONS 权限。 有关 .NET MAUI 权限的详细信息,请参阅“权限”。

配置 iOS 应用

在搭载 Apple Silicon 或 T2 处理器的 Mac 电脑上运行 macOS 13 及更高版本时,iOS 模拟器在 iOS 16 及更高版本中支持远程通知。 每个模拟器都会生成该模拟器和其上运行的 Mac 硬件组合的唯一注册令牌。

重要

模拟器支持 Apple Push Notification Service 沙盒环境。

以下说明假定你使用的是支持在 iOS 模拟器中接收远程通知的硬​​件。 如果不是这种情况,则必须在物理设备上运行 iOS 应用,这将要求你为包含推送通知功能的应用创建一个预置描述文件。 然后,你需要确保应用是使用证书和预置描述文件生成的。 有关如何执行此操作的详细信息,请参阅设置 iOS 应用以使用 Azure 通知中心,然后按照以下说明进行操作。

若要在 iOS 上配置 .NET MAUI 应用以接收和处理推送通知,请执行以下操作:

  1. 在 Visual Studio 中,编辑项目文件 (*.csproj),并将 iOS 的 SupportedOSPlatformVersion 设置为 13.0:

    <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">13.0</SupportedOSPlatformVersion>
    

    Apple 在 iOS 13 中对其推送服务进行了更改。 有关详细信息,请参阅适用于 iOS 13 的 Azure 通知中心更新

  2. 在 Visual Studio 中,将 Entitlements.plist 文件添加到项目的“Platforms/iOS”文件夹中,并将以下 XML 添加到该文件

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <key>aps-environment</key>
      <string>development</string>
    </dict>
    </plist>
    

    这会设置 APS 环境权利,并指定使用开发 Apple Push Notification 服务环境。 在生产应用中,此权利值应设置为 production。 有关此权利的详细信息,请参阅 developer.apple.com 上的 APS 环境权利

    有关添加权利文件的详细信息,请参阅 iOS 权利

  3. 在 Visual Studio 中,将名为 DeviceInstallationService 的新类添加到项目的“Platforms/iOS”文件夹中,并将以下代码添加到文件

    using PushNotificationsDemo.Services;
    using PushNotificationsDemo.Models;
    using UIKit;
    
    namespace PushNotificationsDemo.Platforms.iOS;
    
    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 值)和通知中心注册有效负载。

  4. 在 Visual Studio 中,将名为 NSDataExtensions 的新类添加到项目的“Platforms/iOS”文件夹中,并将以下代码添加到文件

    using Foundation;
    using System.Text;
    
    namespace PushNotificationsDemo.Platforms.iOS;
    
    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();
        }
    }
    

    ToHexString 扩展方法将由要添加的代码使用,用于分析检索到的设备令牌。

  5. 在 Visual Studio 中,打开“Platforms/iOS”文件夹中的 AppDelegate.cs 文件,并添加以下 using 语句

    using System.Diagnostics;
    using Foundation;
    using PushNotificationsDemo.Platforms.iOS;
    using PushNotificationsDemo.Services;
    using UIKit;
    using UserNotifications;
    
  6. AppDelegate 类中,添加支持字段来存储对 IPushDemoNotificationActionServiceINotificationRegistrationServiceIDeviceInstallationService 实现的引用:

    IPushDemoNotificationActionService _notificationActionService;
    INotificationRegistrationService _notificationRegistrationService;
    IDeviceInstallationService _deviceInstallationService;
    
  7. AppDelegate 类中,添加 NotificationActionServiceNotificationRegistrationServiceDeviceInstallationService 私有属性,以便从应用的依赖项注入容器中检索其具体实现:

    IPushDemoNotificationActionService NotificationActionService =>
        _notificationActionService ?? (_notificationActionService = IPlatformApplication.Current.Services.GetService<IPushDemoNotificationActionService>());
    
    INotificationRegistrationService NotificationRegistrationService =>
        _notificationRegistrationService ?? (_notificationRegistrationService = IPlatformApplication.Current.Services.GetService<INotificationRegistrationService>());
    
    IDeviceInstallationService DeviceInstallationService =>
        _deviceInstallationService ?? (_deviceInstallationService = IPlatformApplication.Current.Services.GetService<IDeviceInstallationService>());
    
  8. AppDelegate 类中,添加 CompleteRegistrationAsync 方法以设置 IDeviceInstallationService.Token 属性值:

    Task CompleteRegistrationAsync(NSData deviceToken)
    {
        DeviceInstallationService.Token = deviceToken.ToHexString();
        return NotificationRegistrationService.RefreshRegistrationAsync();
    }
    

    此方法还会刷新注册并缓存设备令牌(如果它自上次存储以来已发生更新)。

  9. AppDelegate 类中,添加用于处理 NSDictionary 通知数据的 ProcessNotificationActions 方法,并有条件地调用 NotificationActionService.TriggerAction

    void ProcessNotificationActions(NSDictionary userInfo)
    {
        if (userInfo == null)
            return;
    
        try
        {
            // If your app isn't in the foreground, the notification goes to Notification Center.
            // If your app is in the foreground, the notification goes directly to your app and you
            // need to process the notification payload yourself.
            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. AppDelegate 类中,通过将 deviceToken 参数传递给 CompleteRegistrationAsync 方法来添加 RegisteredForRemoteNotifications 方法:

    [Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")]
    public void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
    {
        CompleteRegistrationAsync(deviceToken)
            .ContinueWith((task) =>
            {
                if (task.IsFaulted)
                    throw task.Exception;
            });
    }
    

    当应用注册以接收远程通知时,将调用此方法,并用于请求唯一的设备令牌,该令牌实际上是应用在设备上的地址。

  11. AppDelegate 类中,通过将 userInfo 参数传递给 ProcessNotificationActions 方法来添加 ReceivedRemoteNotification 方法:

    [Export("application:didReceiveRemoteNotification:")]
    public void ReceivedRemoteNotification(UIApplication application, NSDictionary userInfo)
    {
        ProcessNotificationActions(userInfo);
    }
    

    当应用收到远程通知时,将调用此方法,并用于处理通知。

  12. AppDelegate 类中,添加 FailedToRegisterForRemoteNotifications 方法以记录任何错误:

    [Export("application:didFailToRegisterForRemoteNotificationsWithError:")]
    public void FailedToRegisterForRemoteNotifications(UIApplication application, NSError error)
    {
        Debug.WriteLine(error.Description);
    }
    

    当无法注册应用以接收远程通知时,将调用此方法。 如果设备未连接到网络、APNS 服务器无法访问或应用配置不正确,则注册可能会失败。

    注意

    对于生产方案,需要在 FailedToRegisterForRemoteNotifications 方法中实现正确的日志记录和错误处理。

  13. AppDelegate 类中,添加 FinishedLaunching 方法,以有条件地请求使用通知并注册远程通知的权限:

    [Export("application:didFinishLaunchingWithOptions:")]
    public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
    {
        if (DeviceInstallationService.NotificationsSupported)
        {
            UNUserNotificationCenter.Current.RequestAuthorization(
                UNAuthorizationOptions.Alert |
                UNAuthorizationOptions.Badge |
                UNAuthorizationOptions.Sound,
                (approvalGranted, error) =>
                {
                    if (approvalGranted && error == null)
                    {
                        MainThread.BeginInvokeOnMainThread(() =>
                        {
                            UIApplication.SharedApplication.RegisterForRemoteNotifications();
                        });
                    }
                });
        }
    
        using (var userInfo = launchOptions?.ObjectForKey(UIApplication.LaunchOptionsRemoteNotificationKey) as NSDictionary)
        {
            ProcessNotificationActions(userInfo);
        }
    
        return base.FinishedLaunching(application, launchOptions);
    }
    

    有关请求使用通知的权限的信息,请参阅 developer.apple.com 上的请求使用通知的权限

有关 iOS 中的通知的信息,请参阅 developer.apple.com 上的 用户通知

使用应用的依赖项注入容器注册类型

  1. 在 Visual Studio 中,打开 MauiProgram.cs 并为 PushNotificationsDemo.Services 命名空间添加 using 语句

    using PushNotificationsDemo.Services;
    
  2. MauiProgram 类中,为 RegisterServices 扩展方法添加代码,该方法在每个平台上注册 DeviceInstallationService,并跨平台注册 PushDemoNotificationActionServiceNotificationRegistrationService 服务,返回 MauiAppBuilder 对象:

    public static MauiAppBuilder RegisterServices(this MauiAppBuilder builder)
    {
    #if IOS
        builder.Services.AddSingleton<IDeviceInstallationService, PushNotificationsDemo.Platforms.iOS.DeviceInstallationService>();
    #elif ANDROID
        builder.Services.AddSingleton<IDeviceInstallationService, PushNotificationsDemo.Platforms.Android.DeviceInstallationService>();
    #endif
    
        builder.Services.AddSingleton<IPushDemoNotificationActionService, PushDemoNotificationActionService>();
        builder.Services.AddSingleton<INotificationRegistrationService>(new NotificationRegistrationService(Config.BackendServiceEndpoint, Config.ApiKey));
    
        return builder;
    }
    
  3. MauiProgram 类中,为 RegisterViews 扩展方法添加代码,该方法将 MainPage 类型注册为单一实例,并返回 MauiAppBuilder 对象:

    public static MauiAppBuilder RegisterViews(this MauiAppBuilder builder)
    {
        builder.Services.AddSingleton<MainPage>();
        return builder;
    }
    

    MainPage 类型已注册,因为它需要 INotificationRegistrationService 依赖项,并且任何需要依赖项的类型都必须注册到依赖项注入容器。

  4. MauiProgram 类中,修改 CreateMauiApp 方法,使其调用 RegisterServicesRegisterViews 扩展方法:

    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            })
            .RegisterServices()
            .RegisterViews();
    
    #if DEBUG
          builder.Logging.AddDebug();
    #endif
          return builder.Build();
    }
    

有关 .NET MAUI 中依赖项注入的详细信息,请参阅依赖项注入

测试应用

可以通过使用后端服务或通过 Azure 门户向应用发送推送通知来测试应用。

在搭载 Apple Silicon 或 T2 处理器的 Mac 电脑上运行 macOS 13 及更高版本时,iOS 模拟器在 iOS 16 及更高版本中支持远程通知。 如果不满足上述硬件要求,则必须在物理设备上测试 iOS 应用。 在 Android 上,可以在开发人员解锁的物理设备或模拟器上测试应用。

当应用在后台运行时,Android 和 iOS 会代表应用显示推送通知。 如果应用在收到通知时在前台运行,则应用的代码将确定该行为。 例如,可以更新应用的接口以反映通知中包含的新信息。

使用后端服务进行测试

若要通过发布到 Azure 应用服务的后端服务向应用发送测试推送通知,请执行以下操作:

  1. 在 Visual Studio 中,运行 Android 或 iOS 上的 PushNotificationsDemo 应用并选择“注册”按钮

    注意

    如果在 Android 上进行测试,请确保未使用调试配置运行。 或者,如果应用之前已部署,请确保它已强制关闭,然后再次从启动器启动应用。

  2. 在所选的 REST 工具中,将 POST 请求发送到以下地址:

    https://<app_name>.azurewebsites.net/api/notifications/requests
    

    确保将请求标头配置为包含键 apikey 及其值,将正文设置为 raw,并使用以下 JSON 内容:

    {
        "text": "Message from REST tooling!",
        "action": "action_a"
    }
    

    总体请求应类似于以下示例:

    POST /api/notifications/requests HTTP/1.1
    Host: https://<app_name>.azurewebsites.net
    apikey: <your_api_key>
    Content-Type: application/json
    
    {
        "text": "Message from REST tooling!",
        "action": "action_a"
    }
    
  3. 在你选择的 REST 工具中,验证是否收到“200 正常”响应

  4. 在 Android 或 iOS 上的应用中,警报应显示“收到 ActionA 操作”

有关调用 REST API 的详细信息,请参阅在 .NET MAUI 中使用 .http 文件使用 Http Repl 测试 Web API。 在 Visual Studio Code 中,REST 客户端可用于测试 REST API。

使用 Azure 门户进行测试

借助 Azure 通知中心,可以检查应用是否可以接收推送通知。

若要通过 Azure 门户向应用发送测试推送通知,请执行以下操作:

  1. 在 Visual Studio 中,运行 Android 或 iOS 上的 PushNotificationsDemo 应用并选择“注册”按钮

    注意

    如果在 Android 上进行测试,请确保未使用调试配置运行。 或者,如果应用之前已部署,请确保它已强制关闭,然后再次从启动器启动应用。

  2. Azure 门户中,浏览到通知中心,然后选择“概览”边栏选项卡上的“测试发送”按钮

  3. 在“测试发送”边栏选项卡中,选择所需的“平台”并修改有效负载

    对于 Apple,请使用以下有效负载:

    {
      "aps": {
        "alert": "Message from Notification Hub!"
      },
      "action": "action_a"
    }
    

    对于 Android,请使用以下有效负载:

    {
      "message": {
        "notification": {
          "title": "PushDemo",
          "body": "Message from Notification Hub!"
        },
        "data": {
          "action": "action_a"
        }
      }
    }
    

    Azure 门户应指示已成功发送通知。

    有关 Firebase Cloud Messaging 消息格式的信息,请参阅 developer.android.com 上的 FCM 消息

  4. 在 Android 或 iOS 上的应用中,警报应显示“收到 ActionA 操作”

故障排除

以下部分讨论了尝试在客户端应用中使用推送通知时遇到的常见问题。

后端服务没有响应

在本地进行测试时,请确保后端服务正在运行,并且正在使用正确的端口。

如果要对 Azure API 应用进行测试,请检查服务是否正在运行、是否已部署并且已正确启动。

确保在 REST 工具或 .NET MAUI 应用配置中正确指定了基址。 在本地进行测试时,基址应为 https://<api_name>.azurewebsites.nethttps://localhost:7020

从后端服务收到 401 状态代码

验证是否正确设置 apikey 请求标头,以及此值是否与你为后端服务配置的值相匹配。

如果在本地进行测试时收到此错误,请确保在 .NET MAUI 应用中定义的密钥值与后端服务使用的 Authentication:ApiKey user-secrets 值匹配。

如果要使用 Azure API 应用进行测试,请确保 .NET MAUI 应用中定义的密钥值与 Azure 门户中定义的 Authentication:ApiKey 应用设置值匹配。 如果在部署后端服务后创建或更改了此应用设置,则必须重启服务才能使该值生效。

从后端服务收到 404 状态代码

验证终结点和 HTTP 请求方法是否正确。

  • PUT - https://<api_name>.azurewebsites.net/api/notifications/installations
  • DELETE - https://<api_name>.azurewebsites.net/api/notifications/installations/<installation_id>
  • POST - https://<api_name>.azurewebsites.net/api/notifications/requests

如果在本地进行测试,应为:

  • PUT - https://localhost:7020/api/notifications/installations
  • DELETE - https://localhost:7020/api/notifications/installations/<installation_id>
  • POST - https://localhost:7020/api/notifications/requests

重要

在 .NET MAUI 应用中指定基址时,请确保它以 / 结尾。 在本地进行测试时,基址应为 https://<api_name>.azurewebsites.nethttps://localhost:7020/

开始或停止调试会话后,Android 上没有收到通知

确保每次启动调试会话时注册。 调试程序将导致生成新的 Firebase 令牌,因此必须更新通知中心安装。

无法注册并显示通知中心错误消息

验证测试设备是否已连接到网络。 然后,通过设置断点来确定 HTTP 响应状态代码,以检查 HttpResponse 中的 StatusCode 属性。

根据状态代码,查看以前的故障排除建议(如果适用)。

在为相应的 API 返回特定状态代码的行上设置断点。 然后,在本地进行调试时,尝试调用后端服务。

通过你选择的 REST 工具验证后端服务是否按预期运行,并使用 .NET MAUI 应用为所选平台创建的有效负载。

查看特定于平台的配置部分,确保没有遗漏任何步骤。 检查是否为所选平台的 InstallationIdToken 变量解析了合适的值。

无法解析有设备错误消息的设备的 ID

查看特定于平台的配置部分,确保没有遗漏任何步骤。