Tutorial: Send push notifications to Xamarin.Forms apps using Azure Notification Hubs via a backend service

Download Sample Download the sample

In this tutorial, you use Azure Notification Hubs to push notifications to a Xamarin.Forms application targeting Android and iOS.

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.

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:

  • An Azure subscription where you can create and manage resources.
  • A Mac with Visual Studio for Mac installed or a PC running Visual Studio 2019.
  • Visual Studio 2019 users must also have Mobile development with .NET and ASP.NET and web development workloads installed.
  • The ability to run the app on either Android (physical or emulator devices) or iOS (physical devices only).

For Android, you must have:

  • A developer unlocked physical device or an emulator (running API 26 and above with Google Play Services installed).

For iOS, you must have:

Note

The iOS Simulator does not support remote notifications and so a physical device is required when exploring this sample on iOS. However, you do not need to run the app on both Android and iOS in order to complete this tutorial.

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.

Important

The steps provided are specific to Visual Studio for Mac. It's possible to follow along using Visual Studio 2019 but there may be some differences to reconcile. For example, descriptions of user interface and workflows, template names, environment configuration, and so on.

Set up Push Notification Services and Azure Notification Hub

In this section, you set up Firebase Cloud Messaging (FCM) and Apple Push Notification Services (APNS). You then create and configure a notification hub to work with those services.

Create a Firebase project and enable Firebase Cloud Messaging for Android

  1. Sign in to the Firebase console. Create a new Firebase project entering PushDemo as the Project name.

    Note

    A unique name will be generated for you. By default this is comprised of a lowercase variant of the name you provided plus a generated number separated by a dash. You can change this if you want provided it is still globally unique.

  2. After you create your project, select Add Firebase to your Android app.

    Add Firebase to your Android app

  3. On the Add Firebase to your Android app page, take the following steps.

    1. For the Android package name, enter a name for your package. For example: com.<organization_identifier>.<package_name>.

      Specify the package name

    2. Select Register app.

    3. Select Download google-services.json. Then save the file into a local folder for use later on and select Next.

      Download google-services.json

    4. Select Next.

    5. Select Continue to console

      Note

      If the Continue to console button is not enabled, due to the verify installation check, then choose Skip this step.

  4. In the Firebase console, select the cog for your project. Then select Project Settings.

    Select Project Settings

    Note

    If you haven't downloaded the google-services.json file, you can do download it on this page.

  5. Switch to the Cloud Messaging tab at the top. Copy and save the Server key for later use. You use this value to configure your notification hub.

    Copy server key

Register your iOS app for push notifications

To send push notifications to an iOS app, register your application with Apple, and also register for push notifications.

  1. If you haven't already registered your app, browse to the iOS Provisioning Portal at the Apple Developer Center. Sign in to the portal with your Apple ID, navigate to Certificates, Identifiers & Profiles, then select Identifiers. Click + to register a new app.

    iOS Provisioning Portal App IDs page

  2. On the Register a New Identifier screen, select the App IDs radio button. Then select Continue.

    iOS Provisioning Portal register new ID page

  3. Update the following three values for your new app, and then select Continue:

    • Description: Type a descriptive name for your app.

    • Bundle ID: Enter a Bundle ID of the form com.<organization_identifier>.<product_name> as mentioned in the App Distribution Guide. In the following screenshot, the mobcat value is used as an organization identifier and the PushDemo value is used as the product name.

      iOS Provisioning Portal register app ID page

    • Push Notifications: Check the Push Notifications option in the Capabilities section.

      Form to register a new App ID

      This action generates your App ID and requests that you confirm the information. Select Continue, then select Register to confirm the new App ID.

      Confirm new App ID

      After you select Register, you see the new App ID as a line item in the Certificates, Identifiers & Profiles page.

  4. In the Certificates, Identifiers & Profiles page, under Identifiers, locate the App ID line item that you created. Then, select its row to display the Edit your App ID Configuration screen.

Creating a certificate for Notification Hubs

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. Creating a p12 push certificate that can be uploaded directly to Notification Hub (the original approach)

  2. Creating a p8 certificate that can be used for token-based authentication (the newer and recommended approach)

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.

OPTION 1: Creating a p12 push certificate that can be uploaded directly to Notification Hub
  1. On your Mac, run the Keychain Access tool. It can be opened from the Utilities folder or the Other folder on the Launchpad.

  2. Select Keychain Access, expand Certificate Assistant, and then select Request a Certificate from a Certificate Authority.

    Use Keychain Access to request a new certificate

    Note

    By default, Keychain Access selects the first item in the list. 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. 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. Select your User Email Address, enter your Common Name value, make sure that you specify Saved to disk, and then select Continue. Leave CA Email Address blank as it isn't required.

    Expected certificate information

  4. Enter a name for the Certificate Signing Request (CSR) file in Save As, select the location in Where, and then select Save.

    Choose a file name for the certificate

    This action saves the CSR file in the selected location. The default location is Desktop. Remember the location chosen for the file.

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

    Edit App ID page

  6. The Apple Push Notification service TLS/SSL Certificates window appears. Select the Create Certificate button under the Development TLS/SSL Certificate section.

    Create certificate for App ID button

    The Create a new Certificate screen is displayed.

    Note

    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. 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. After the portal creates the certificate, select the Download button. Save the certificate, and remember the location to which it's saved.

    Generated certificate download page

    The certificate is downloaded and saved to your computer in your Downloads folder.

    Locate certificate file in the Downloads folder

    Note

    By default, the downloaded development certificate is named aps_development.cer.

  9. Double-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 certificates list showing new certificate

    Note

    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. In Keychain Access, Control + Click on the new push certificate that you created in the Certificates category. Select Export, name the file, select the p12 format, and then select Save.

    Export certificate as p12 format

    You can choose to protect the certificate with a password, but a password is optional. Click OK if you want to bypass password creation. Make a note of the file name and location of the exported p12 certificate. They're used to enable authentication with APNs.

    Note

    Your p12 file name and location might be different than what is pictured in this tutorial.

OPTION 2: Creating a p8 certificate that can be used for token-based authentication
  1. Make note of the following details:

    • App ID Prefix (Team ID)
    • Bundle ID
  2. Back in Certificates, Identifiers & Profiles, click Keys.

    Note

    If you already have a key configured for APNS, you can re-use the p8 certificate that you downloaded right after it was created. 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. 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. Click Download and then move the p8 file (prefixed with AuthKey_) to a secure local directory, then click Done.

    Note

    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. Make note of the Key ID value.

  8. Open your p8 certificate in a suitable application of your choice such as Visual Studio Code. Make note of the key value (between -----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----).

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

    Note

    This is the token value that will be used later to configure Notification Hub.

At the end of these steps, you should have the following information for use later in Configure your notification hub with APNS information:

  • Team ID (see step 1)
  • Bundle ID (see step 1)
  • Key ID (see step 7)
  • Token value (p8 key value obtained in step 8)

Create a provisioning profile for the app

  1. Return to the iOS Provisioning Portal, select Certificates, Identifiers & Profiles, select Profiles from the left menu, and then select + to create a new profile. The Register a New Provisioning Profile screen appears.

  2. Select iOS App Development under Development as the provisioning profile type, and then select Continue.

    Provisioning profile list

  3. Next, select the app ID you created from the App ID drop-down list, and select Continue.

    Select the App ID

  4. In the Select certificates window, select the development certificate that you use for code signing, and select Continue.

    Note

    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. Developer certificates can be created in the Apple Developer Portal, via Xcode or in Visual Studio.

  5. Return to the Certificates, Identifiers & Profiles page, select Profiles from the left menu, and then select + to create a new 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. Next, select the devices to use for testing, and select Continue.

  8. Finally, choose a name for the profile in Provisioning Profile Name, and select Generate.

    Choose a provisioning profile name

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

In this section, you create a notification hub and configure authentication with APNS. You can use a p12 push certificate or token-based authentication. If you want to use a notification hub that you've already created, you can skip to step 5.

  1. Sign 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

    Note

    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

    Note

    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.

Configure your Notification Hub with APNS information

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.

Note

Use the Production for Application Mode only if you want to send push notifications to users who purchased your app from the store.

OPTION 1: Using a .p12 push certificate

  1. Select Certificate.

  2. Select the file icon.

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

OPTION 2: Using token-based authentication

  1. Select Token.

  2. Enter the following values that you acquired earlier:

    • Key ID
    • Bundle ID
    • Team ID
    • Token
  3. Choose Sandbox.

  4. Select Save.

Configure your notification hub with FCM information

  1. Select Google (GCM/FCM) in the Settings section on the left menu.
  2. Enter the server key you noted down from the Google Firebase Console.
  3. Select Save on the toolbar.

Create an ASP.NET Core Web API backend application

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.

Create a web project

  1. In Visual Studio, select File > New Solution.

  2. Select .NET Core > App > ASP.NET Core > API > Next.

  3. In the Configure your new ASP.NET Core Web API dialog, select Target Framework of .NET Core 3.1.

  4. Enter PushDemoApi for the Project Name and then select Create.

  5. Start debugging (Command + Enter) to test the templated app.

    Note

    The templated app is configured to use the WeatherForecastController as the launchUrl. This is set in Properties > launchSettings.json.

    If you are prompted with an Invalid development certificate found message:

    1. Click Yes to agree to running the 'dotnet dev-certs https' tool to fix this. 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. Expand the Controllers folder, then delete WeatherForecastController.cs.

  7. Delete WeatherForecast.cs.

  8. 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. Otherwise, you can look them up in Azure.

    NotificationHub:Name:
    See Name in the Essentials summary at the top of Overview.

    NotificationHub:ConnectionString:
    See DefaultFullSharedAccessSignature in Access Policies

    Note

    For production scenarios, you can look at options such as Azure KeyVault to securely store the connection string. For simplicity, the secrets will be added to the Azure App Service application settings.

Authenticate clients using an API Key (Optional)

API keys aren't as secure as tokens, but will suffice for the purposes of this tutorial. An API key can be configured easily via the ASP.NET Middleware.

  1. Add the API key to the local configuration values.

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

    Note

    You should replace the placeholder value with your own and make a note of it.

  2. Control + Click on the PushDemoApi project, choose New Folder from the Add menu, then click Add using Authentication as the Folder Name.

  3. Control + Click on the Authentication folder, then choose New File... from the Add menu.

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

    Note

    An Authentication Handler is a type that implements the behavior of a scheme, in this case a custom API Key scheme.

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

    Note

    This extension method simplifies the middleware configuration code in Startup.cs making it more readable and generally easier to follow.

  7. In Startup.cs, update the ConfigureServices method to configure the API Key authentication below the call to the services.AddControllers method.

    using PushDemoApi.Authentication;
    using PushDemoApi.Models;
    using PushDemoApi.Services;
    
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
    
        services.AddAuthentication(options =>
        {
            options.DefaultAuthenticateScheme = ApiKeyAuthOptions.DefaultScheme;
            options.DefaultChallengeScheme = ApiKeyAuthOptions.DefaultScheme;
        }).AddApiKeyAuth(Configuration.GetSection("Authentication").Bind);
    }
    
  8. Still in Startup.cs, update the Configure method to call the UseAuthentication and UseAuthorization extension methods on the app's IApplicationBuilder. 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();
        });
    }
    

    Note

    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 supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.

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 + Click on the Dependencies folder, then choose Manage NuGet Packages....

  2. 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 + Click on the PushDemoApi project, choose New Folder from the Add menu, then click Add using Models as the Folder Name.

  5. Control + Click on the Models folder, then choose New File... from the Add menu.

  6. 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)\" }";
            }
        }
    }
    

    Note

    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. Add another Empty Class to the Models folder called DeviceInstallation.cs, then add 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. 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. 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. Add a new folder to the PushDemoApi project called Services.

  11. Add an Empty Interface to the Services folder called INotificationService.cs, then add the following implementation.

    using System.Threading;
    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. 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);
            }
        }
    }
    

    Note

    The tag expression provided to SendTemplateNotificationAsync is limited to 20 tags. It is limited to 6 for most operators but the expression contains only ORs (||) in this case. 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. In Startup.cs, update the ConfigureServices method to add the NotificationHubsService as a singleton implementation of INotificationService.

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

Create the notifications API

  1. Control + Click on the Controllers folder, then choose New File... from the Add menu.

  2. Select ASP.NET Core > Web API Controller Class, enter NotificationsController for the Name, then click New.

    Note

    If you're following with Visual Studio 2019, choose the API Controller with read/write actions template.

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

    Note

    The Controller base class provides support for views but this is not needed in this case and so ControllerBase can be used instead. If you're following with Visual Studio 2019, you can skip this step.

  5. 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. 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. 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. Start debugging (Command + Enter) to validate the app is working with the new NotificationsController and returns a 401 Unauthorized status.

    Note

    Visual Studio may not automatically launch the app in the browser. You will use Postman to test the API from this point on.

  9. On a new Postman tab, set the request to GET. Enter the address below replacing the placeholder <applicationUrl> with the https applicationUrl found in Properties > launchSettings.json.

    <applicationUrl>/api/notifications
    

    Note

    The applicationUrl should be 'https://localhost:5001' for the default profile. If you're using IIS (default in Visual Studio 2019 on Windows), you should use the applicationUrl specified in the iisSettings item instead. You will receive a 404 response if the address is incorrect.

  10. 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
    apikey <your_api_key>
  11. Click the Send button.

    Note

    You should receive a 200 OK status with some JSON content.

    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 in NotificationsController.cs 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();
    }
    

Create the API app

You now create an API App in Azure App Service for hosting the backend service.

  1. Sign in to the Azure portal.

  2. Click Create a resource, then search for and choose API App, then click Create.

  3. Update the following fields, then click Create.

    App name:
    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

    Note

    Change from the default option to a plan that includes SSL support. Otherwise, you will need to take the appropriate steps when working with the mobile app to prevent http requests from getting blocked.

    Application Insights:
    Keep the suggested option (a new resource will be created using that name) or pick an existing resource.

  4. Once the API App has been provisioned, navigate to that resource.

  5. Make note of the URL property in the Essentials summary at the top of the Overview. This URL is your backend endpoint that will be used later in this tutorial.

    Note

    The URL uses the API app name that you specified earlier, with the format https://<app_name>.azurewebsites.net.

  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>
    NotificationHub:Name <hub_name_value>
    NotificationHub:ConnectionString <hub_connection_string_value>

    Note

    These are the same settings you defined previously in the user settings. You should be able to copy these over. The Authentication:ApiKey setting is required only if you chose to to complete the Authenticate clients using an API Key section. 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

Next, you deploy the app to the API App to make it accessible from all devices.

Note

The following steps are specific to Visual Studio for Mac. If you're following with Visual Studio 2019 on Windows, the publishing flow will be different. See Publish to Azure App Service on Windows.

  1. Change your configuration from Debug to Release if you haven't already done so.

  2. 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. Use the account that you used in the previous create the API App section.

  4. Select the Azure App Service API App you created previously from the list as your publish target, and then click Publish.

After you've completed the wizard, it publishes the app to Azure and then opens the app. Make a note of the URL if you haven't done so already. This URL is your backend endpoint that is used later in this tutorial.

Validating the published API

  1. In Postman open a new tab, set the request to PUT 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>.azurewebsites.net/api/notifications/installations
    

    Note

    The base address should be in the format https://<app_name>.azurewebsites.net/

  2. 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
    apikey <your_api_key>
  3. Choose the raw option for the Body, then choose JSON from the list of format options, and then include some placeholder JSON content:

    {}
    
  4. Click Send.

    Note

    You should receive a 422 UnprocessableEntity status from the service.

  5. Do steps 1-4 again but this time specifying the requests endpoint to validate you receive a 400 Bad Request response.

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

Note

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.

Create a cross-platform Xamarin.Forms application

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.

Note

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.

Create the Xamarin.Forms solution

  1. In Visual Studio, create a new Xamarin.Forms solution using Blank Forms App as the template and entering PushDemo for the Project Name.

    Note

    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 + Click on the PushDemo solution, then choose Update NuGet Packages.

  3. Control + Click on the PushDemo solution, then choose Manage NuGet Packages...

  4. Search for Newtonsoft.Json and ensure it's checked.

  5. Click Add Packages, then click Accept when prompted to accept the license terms.

  6. 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 + Click on the PushDemo project, choose New Folder from the Add menu, then click Add using Models as the Folder Name.

  2. Control + Click on the Models folder, then choose New File... from the Add menu.

  3. 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. Add an Empty Enumeration to the Models folder called PushDemoAction.cs with the following implementation.

    namespace PushDemo.Models
    {
        public enum PushDemoAction
        {
            ActionA,
            ActionB
        }
    }
    
  5. 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}'");
               }
           }
       }
    }
    

    Note

    This is a trimmed-down version of the ServiceContainer class from the XamCAT repository. It will be used as a light-weight IoC (Inversion of Control) container.

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

    Note

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

    Note

    This will handle the interaction between the client and backend service.

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

    Note

    This is used as a simple mechanism to centralize the handling of notification actions.

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

    Note

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

    Note

    The apiKey argument is only required if you chose to complete the Authenticate clients using an API Key section.

  11. 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. 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";
        }
    }
    

    Note

    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.

    The ApiKey field is only required if you chose to complete the Authenticate clients using an API Key section.

  13. 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>";
            }
        }
    }
    

    Note

    Replace the placeholder values with your own. You should have made a note of these when you built the backend service. The API App URL should be https://<api_app_name>.azurewebsites.net/. Remember to add *.local_secrets.* to your gitignore file to avoid committing this file.

    The ApiKey field is only required if you chose to complete the Authenticate clients using an API Key section.

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

    Note

    The Begin method will be called by each platform when the app launches passing in a platform-specific implementation of IDeviceInstallationService.

    The NotificationRegistrationService apiKey constructor argument is only required if you chose to complete the Authenticate clients using an API Key section.

Implement the cross-platform UI

  1. 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. Now in MainPage.xaml.cs, add a readonly backing field to store a reference to the INotificationRegistrationService implementation.

    readonly INotificationRegistrationService _notificationRegistrationService;
    
  3. 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. 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. Now in App.xaml.cs, ensure the following namespaces are referenced.

    using PushDemo.Models;
    using PushDemo.Services;
    using Xamarin.Essentials;
    using Xamarin.Forms;
    
  6. 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. 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();
    }
    

    Note

    This is simply to demonstrate the receipt and propagation of push notification actions. 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.

Configure the native Android project for push notifications

Validate package name and permissions

  1. In PushDemo.Android, open the Project Options then Android Application from the Build section.

  2. Check that the Package name matches the value you used in the Firebase Console PushDemo project. The Package name was in the format com.<organization>.pushdemo.

  3. Set the Minimum Android Version to Android 8.0 (API level 26) and the Target Android Version to the latest API level.

    Note

    Only those devices running API level 26 and above are supported for the purposes of this tutorial however you can extend it to support devices running older versions.

  4. Ensure the INTERNET and READ_PHONE_STATE permissions are enabled under Required permissions.

  5. Click OK

Add the Xamarin Google Play Services base and Xamarin.Firebase.Messaging packages

  1. In PushDemo.Android, Control + Click on the Packages folder, then choose Manage NuGet Packages....

  2. Search for Xamarin.GooglePlayServices.Base (not Basement) and ensure it's checked.

  3. Search for Xamarin.Firebase.Messaging and ensure it's checked.

  4. Click Add Packages, then click Accept when prompted to accept the license terms.

Add the Google Services JSON file

  1. Control + Click on the PushDemo.Android project, then choose Existing File... from the Add menu.

  2. Choose the google-services.json file you downloaded earlier when you set up the PushDemo project in the Firebase Console then click Open.

  3. When prompted, choose to Copy the file to the directory.

  4. Control + Click on the google-services.json file from within the PushDemo.Android project, then ensure GoogleServicesJson is set as the Build Action.

Handle push notifications for Android

  1. Control + Click on the PushDemo.Android project, choose New Folder from the Add menu, then click Add using Services as the Folder Name.

  2. Control + Click on the Services folder, then choose New File... from the Add menu.

  3. Select General > Empty Class, enter DeviceInstallationService.cs for the Name, then click New adding the following implementation.

    using System;
    using Android.App;
    using Android.Gms.Common;
    using PushDemo.Models;
    using PushDemo.Services;
    using static Android.Provider.Settings;
    
    namespace PushDemo.Droid.Services
    {
        public class DeviceInstallationService : IDeviceInstallationService
        {
            public string Token { get; set; }
    
            public bool NotificationsSupported
                => GoogleApiAvailability.Instance
                    .IsGooglePlayServicesAvailable(Application.Context) == ConnectionResult.Success;
    
            public string GetDeviceId()
                => Secure.GetString(Application.Context.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 FCM");
    
                var installation = new DeviceInstallation
                {
                    InstallationId = GetDeviceId(),
                    Platform = "fcm",
                    PushChannel = Token
                };
    
                installation.Tags.AddRange(tags);
    
                return installation;
            }
    
            string GetPlayServicesError()
            {
                int resultCode = GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(Application.Context);
    
                if (resultCode != ConnectionResult.Success)
                    return GoogleApiAvailability.Instance.IsUserResolvableError(resultCode) ?
                               GoogleApiAvailability.Instance.GetErrorString(resultCode) :
                               "This device is not supported";
    
                return "An error occurred preventing the use of push notifications";
            }
        }
    }
    

    Note

    This class provides a unique ID (using Secure.AndroidId) as part of the notification hub registration payload.

  4. Add another Empty Class to the Services folder called PushNotificationFirebaseMessagingService.cs, then add the following implementation.

    using Android.App;
    using Android.Content;
    using Firebase.Messaging;
    using PushDemo.Services;
    
    namespace PushDemo.Droid.Services
    {
        [Service]
        [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })]
        public class PushNotificationFirebaseMessagingService : FirebaseMessagingService
        {
            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>());
    
            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)
            {
                if(message.Data.TryGetValue("action", out var messageAction))
                    NotificationActionService.TriggerAction(messageAction);
            }
        }
    }
    
  5. In MainActivity.cs, ensure the following namespaces have been added to the top of the file.

    using System;
    using Android.App;
    using Android.Content;
    using Android.Content.PM;
    using Android.OS;
    using Android.Runtime;
    using Firebase.Iid;
    using PushDemo.Droid.Services;
    using PushDemo.Services;
    
  6. In MainActivity.cs, set the LaunchMode to SingleTop so MainActivity won't get created again when opened.

    [Activity(
        Label = "PushDemo",
        LaunchMode = LaunchMode.SingleTop,
        Icon = "@mipmap/icon",
        Theme = "@style/MainTheme",
        MainLauncher = true,
        ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    
  7. Add private properties and corresponding backing fields to store a reference to the IPushNotificationActionService and IDeviceInstallationService implementations.

    IPushDemoNotificationActionService _notificationActionService;
    IDeviceInstallationService _deviceInstallationService;
    
    IPushDemoNotificationActionService NotificationActionService
        => _notificationActionService ??
            (_notificationActionService =
            ServiceContainer.Resolve<IPushDemoNotificationActionService>());
    
    IDeviceInstallationService DeviceInstallationService
        => _deviceInstallationService ??
            (_deviceInstallationService =
            ServiceContainer.Resolve<IDeviceInstallationService>());
    
  8. Implement the IOnSuccessListener interface to retrieve and store the Firebase token.

    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity, Android.Gms.Tasks.IOnSuccessListener
    {
        ...
    
        public void OnSuccess(Java.Lang.Object result)
            => DeviceInstallationService.Token =
                result.Class.GetMethod("getToken").Invoke(result).ToString();
    }
    
  9. Add a new method called ProcessNotificationActions that will check whether a given Intent has an extra value named action. Conditionally trigger that action using the IPushDemoNotificationActionService implementation.

    void ProcessNotificationActions(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);
        }
    }
    
  10. Override the OnNewIntent method to call ProcessNotificationActions method.

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

    Note

    Since the LaunchMode for the Activity is set to SingleTop, an Intent will be sent to the existing Activity instance via the OnNewIntent method rather than the OnCreate method and so you must handle an incoming intent in both OnCreate and OnNewIntent methods.

  11. Update the OnCreate method to call Bootstrap.Begin right after the call to base.OnCreate passing in the platform-specific implementation of IDeviceInstallationService.

    Bootstrap.Begin(() => new DeviceInstallationService());
    
  12. In the same method, conditionally call GetInstanceId on the FirebaseApp instance, right after the call to Bootstrap.Begin, adding MainActivity as the IOnSuccessListener.

    if (DeviceInstallationService.NotificationsSupported)
    {
        FirebaseInstanceId.GetInstance(Firebase.FirebaseApp.Instance)
            .GetInstanceId()
            .AddOnSuccessListener(this);
    }
    
  13. Still in OnCreate, call ProcessNotificationActions immediately after the call to LoadApplication passing in the current Intent.

    ...
    
    LoadApplication(new App());
    
    ProcessNotificationActions(Intent);
    

Note

You must re-register the app each time you run it and stop it from a debug session to continue receiving push notifications.

Configure the native iOS project for push notifications

Configure Info.plist and Entitlements.plist

  1. 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. 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. The BundleIdentifier was in the format com.<organization>.PushDemo.

  3. In the same file, set Minimum system version to 13.0.

    Note

    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. Open the Project Options for PushDemo.iOS (double-click on the project).

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

    Note

    If your Signing Certificate and Provisioning Profile have not been automatically selected, choose Manual Provisioning, then click on Bundle Signing Options. 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. 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.

Handle push notifications for iOS

  1. 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 + Click on the Services folder, then choose New File... from the Add menu.

  3. 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";
            }
        }
    }
    

    Note

    This class provides a unique ID (using the UIDevice.IdentifierForVendor value) and the notification hub registration payload.

  4. 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. 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. 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. 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. 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. 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. 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. Override the ReceivedRemoteNotification method passing the userInfo argument to the ProcessNotificationActions method.

    public override void ReceivedRemoteNotification(
        UIApplication application,
        NSDictionary userInfo)
        => ProcessNotificationActions(userInfo);
    
  12. Override the FailedToRegisterForRemoteNotifications method to log the error.

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

    Note

    This is very much a placeholder. You will want to implement proper logging and error handling for production scenarios.

  13. Update 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. 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. 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. Open a new tab in Postman.

  2. Set the request to POST, and enter the following address:

    https://<app_name>.azurewebsites.net/api/notifications/requests
    
  3. 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
    apikey <your_api_key>
  4. 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. 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>.azurewebsites.net
    apikey: <your_api_key>
    Content-Type: application/json
    
    {
        "text": "Message from backend service",
        "action": "action_a"
    }
    
  6. Run the PushDemo application on one or both of the target platforms (Android and iOS).

    Note

    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. In the PushDemo app, tap on the Register button.

  8. Back in Postman, close the Generate Code Snippets window (if you haven't done so already) then click the Send button.

  9. Validate that you get a 200 OK response in Postman and the alert appears in the app showing ActionA action received.

  10. Close the PushDemo app, then click the Send button again in Postman.

  11. Validate that you get a 200 OK response in Postman again. Validate that a notification appears in the notification area for the PushDemo app with the correct message.

  12. Tap on the notification to confirm that it opens the app and displayed the ActionA action received alert.

  13. 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. With the app still open, click the Send button in Postman.

  15. 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. Close the PushDemo app, then click the Send button again in Postman.

  17. Validate that you get a 200 OK response in Postman and that the silent notification doesn't appear in the notification area.

Troubleshooting

No response from the backend service

When testing locally, ensure that the backend service is running and is using the correct port.

If testing against the Azure API App, check the service is running and has been deployed and has started without error.

Be sure to check you've specified the base address correctly in Postman or in the mobile app configuration when testing via the client. The base address should indicatively be https://<api_name>.azurewebsites.net/ or https://localhost:5001/ when testing locally.

Not receiving notifications on Android after starting or stopping a debug session

Ensure you register again after starting or stopping a debug session. The debugger will cause a new Firebase token to be generated. The notification hub installation must be updated as well.

Receiving a 401 status code from the backend service

Validate that you're setting the apikey request header and this value matches the one you had configured for the backend service.

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.

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.

Note

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.

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.

Receiving a 404 status code from the backend service

Validate that the endpoint and HTTP request method is correct. For example, the endpoints should indicatively be:

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

Or when testing locally:

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

When specifying the base address in the client app, ensure it ends with a /. The base address should indicatively be https://<api_name>.azurewebsites.net/ 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. 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.

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.

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. Check that suitable values are being resolved for installation id and token variables for the appropriate platform.

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

Next steps

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 can be quickly incorporated into mobile apps providing analytics and diagnostics to aid in troubleshooting.