maui-push-notifications

Push Notifications for .NET MAUI

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "maui-push-notifications" with this command: npx skills add davidortinau/maui-skills/davidortinau-maui-skills-maui-push-notifications

Push Notifications for .NET MAUI

Architecture

MAUI app → ASP.NET Core backend → Azure Notification Hub → FCM (Android) / APNS (iOS) → device.

Step 1 — Create Azure Notification Hub

  • Azure Portal → create a Notification Hub inside a Notification Hub Namespace.

  • Apple (APNS) → upload .p8 key or .p12 cert; set mode to Sandbox for dev.

  • Google (FCM V1) → paste the FCM V1 service-account JSON from Firebase Console → Project Settings → Cloud Messaging.

  • Copy DefaultFullSharedAccessSignature and hub name for backend appsettings.json .

Step 2 — ASP.NET Core backend API

2.1 NuGet package

<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.*" />

2.2 appsettings.json

{ "NotificationHub": { "Name": "<hub-name>", "ConnectionString": "Endpoint=sb://...;SharedAccessKeyName=DefaultFullSharedAccessSignature;SharedAccessKey=..." }, "Authentication": { "ApiKey": "<random-guid-or-secret>" } }

2.3 Models

namespace YOUR_NAMESPACE.Backend.Models;

public class DeviceInstallation { public string InstallationId { get; set; } = ""; public string Platform { get; set; } = ""; // "fcmv1" or "apns" public string PushChannel { get; set; } = ""; // device token public List<string> Tags { get; set; } = []; }

public class NotificationRequest { public string Title { get; set; } = ""; public string Body { get; set; } = ""; public List<string> Tags { get; set; } = []; }

2.4 NotificationHubService

namespace YOUR_NAMESPACE.Backend.Services;

using Microsoft.Azure.NotificationHubs; using Microsoft.Extensions.Options;

public class NotificationHubOptions { public string Name { get; set; } = ""; public string ConnectionString { get; set; } = ""; }

public interface INotificationService { Task<bool> CreateOrUpdateInstallationAsync(DeviceInstallation device, CancellationToken ct); Task<bool> DeleteInstallationByIdAsync(string installationId, CancellationToken ct); Task<bool> RequestNotificationAsync(NotificationRequest request, CancellationToken ct); }

public class NotificationHubService : INotificationService { readonly NotificationHubClient _hub;

public NotificationHubService(IOptions&#x3C;NotificationHubOptions> options)
{
    _hub = NotificationHubClient.CreateClientFromConnectionString(
        options.Value.ConnectionString, options.Value.Name);
}

public async Task&#x3C;bool> CreateOrUpdateInstallationAsync(DeviceInstallation device, CancellationToken ct)
{
    var installation = new Installation
    {
        InstallationId = device.InstallationId,
        PushChannel = device.PushChannel,
        Tags = device.Tags,
        Platform = device.Platform switch
        {
            "fcmv1" => NotificationPlatform.FcmV1,
            "apns"  => NotificationPlatform.Apns,
            _ => throw new ArgumentException($"Unknown platform: {device.Platform}")
        }
    };
    await _hub.CreateOrUpdateInstallationAsync(installation, ct);
    return true;
}

public async Task&#x3C;bool> DeleteInstallationByIdAsync(string installationId, CancellationToken ct)
{
    await _hub.DeleteInstallationAsync(installationId, ct);
    return true;
}

public async Task&#x3C;bool> RequestNotificationAsync(NotificationRequest request, CancellationToken ct)
{
    // GOTCHA: Azure NH limits tag expressions to 20 tags — batch accordingly.
    var batches = request.Tags.Chunk(20);
    foreach (var batch in batches)
    {
        var tagExpr = string.Join(" || ", batch);
        var fcm = $$"""{"message":{"notification":{"title":"{{request.Title}}","body":"{{request.Body}}"}}}""";
        var apns = $$"""{"aps":{"alert":{"title":"{{request.Title}}","body":"{{request.Body}}"}}}""";
        await Task.WhenAll(
            _hub.SendFcmV1NativeNotificationAsync(fcm, tagExpr, ct),
            _hub.SendAppleNativeNotificationAsync(apns, tagExpr, ct));
    }
    return true;
}

}

2.5 Minimal API endpoints (Program.cs)

builder.Services.Configure<NotificationHubOptions>( builder.Configuration.GetSection("NotificationHub")); builder.Services.AddSingleton<INotificationService, NotificationHubService>();

var app = builder.Build(); var apiKey = builder.Configuration["Authentication:ApiKey"]!;

var api = app.MapGroup("/api/notifications") .AddEndpointFilter(async (ctx, next) => { if (!ctx.HttpContext.Request.Headers.TryGetValue("apikey", out var key) || key != apiKey) return Results.Unauthorized(); return await next(ctx); });

api.MapPut("/installations", async (DeviceInstallation device, INotificationService svc, CancellationToken ct) => await svc.CreateOrUpdateInstallationAsync(device, ct) ? Results.Ok() : Results.BadRequest());

api.MapDelete("/installations/{id}", async (string id, INotificationService svc, CancellationToken ct) => await svc.DeleteInstallationByIdAsync(id, ct) ? Results.Ok() : Results.BadRequest());

api.MapPost("/requests", async (NotificationRequest req, INotificationService svc, CancellationToken ct) => await svc.RequestNotificationAsync(req, ct) ? Results.Ok() : Results.BadRequest());

app.Run();

Step 3 — MAUI client shared code

3.1 Config

namespace YOUR_NAMESPACE;

public static class PushConfig { // GOTCHA: trailing slash required — HttpClient.BaseAddress must end with "/". public const string BackendServiceEndpoint = "https://<your-backend>.azurewebsites.net/"; public const string ApiKey = "<same-key-as-backend>"; }

3.2 IPushNotificationService

namespace YOUR_NAMESPACE.Services;

public interface IPushNotificationService { string Token { get; set; } Task RegisterAsync(CancellationToken ct = default); Task DeregisterAsync(CancellationToken ct = default); }

3.3 PushNotificationService

namespace YOUR_NAMESPACE.Services;

using System.Net.Http.Json; using System.Text.Json;

public class PushNotificationService : IPushNotificationService { static readonly JsonSerializerOptions _json = new(JsonSerializerDefaults.Web); readonly HttpClient _http; public string Token { get; set; } = "";

public PushNotificationService()
{
    _http = new HttpClient { BaseAddress = new Uri(PushConfig.BackendServiceEndpoint) };
    _http.DefaultRequestHeaders.Add("apikey", PushConfig.ApiKey);
}

public async Task RegisterAsync(CancellationToken ct = default)
{
    if (string.IsNullOrWhiteSpace(Token)) return;
    var installation = new
    {
        installationId = GetInstallationId(),
        platform = DeviceInfo.Platform == DevicePlatform.Android ? "fcmv1" : "apns",
        pushChannel = Token,
        tags = new[] { $"user:{GetUserId()}" }
    };
    await _http.PutAsJsonAsync("api/notifications/installations", installation, _json, ct);
}

public async Task DeregisterAsync(CancellationToken ct = default)
{
    await _http.DeleteAsync($"api/notifications/installations/{GetInstallationId()}", ct);
}

string GetInstallationId()
{
    var id = Preferences.Get("installation_id", string.Empty);
    if (string.IsNullOrEmpty(id))
    {
        id = Guid.NewGuid().ToString();
        Preferences.Set("installation_id", id);
    }
    return id;
}

string GetUserId() => "default-user"; // Replace with your auth identity.

}

3.4 DI registration (MauiProgram.cs)

builder.Services.AddSingleton<IPushNotificationService, PushNotificationService>();

Step 4 — Android setup

4.1 Firebase project

  • Firebase Console → Add Android app with your package name.

  • Download google-services.json → place in Platforms/Android/.

  • In .csproj : <GoogleServicesJson Include="Platforms\Android\google-services.json" />

4.2 NuGet packages (Android)

<ItemGroup Condition="'$(TargetFramework)' == 'net9.0-android'"> <PackageReference Include="Xamarin.Firebase.Messaging" Version="124." /> <PackageReference Include="Xamarin.Google.Dagger" Version="2." /> </ItemGroup>

4.3 AndroidManifest.xml additions

<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <application> <service android:name=".PushNotificationFirebaseMessagingService" android:exported="false"> <intent-filter> <action android:name="com.google.firebase.MESSAGING_EVENT" /> </intent-filter> </service> <meta-data android:name="com.google.firebase.messaging.default_notification_channel_id" android:value="default_channel" /> </application>

4.4 MainActivity.cs

namespace YOUR_NAMESPACE;

using Android; using Android.App; using Android.Content; using Android.Content.PM; using Android.Gms.Tasks; using Android.OS; using Firebase.Messaging;

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)] public class MainActivity : MauiAppCompatActivity, IOnSuccessListener { protected override void OnCreate(Bundle? savedInstanceState) { base.OnCreate(savedInstanceState); CreateNotificationChannel(); if (Build.VERSION.SdkInt >= BuildVersionCodes.Tiramisu) RequestPermissions(new[] { Manifest.Permission.PostNotifications }, 0); FirebaseMessaging.Instance.GetToken().AddOnSuccessListener(this); }

public void OnSuccess(Java.Lang.Object result)
{
    var svc = IPlatformApplication.Current!.Services.GetRequiredService&#x3C;IPushNotificationService>();
    svc.Token = result.ToString()!;
    _ = svc.RegisterAsync();
}

void CreateNotificationChannel()
{
    if (Build.VERSION.SdkInt &#x3C; BuildVersionCodes.O) return;
    var channel = new NotificationChannel("default_channel", "General", NotificationImportance.Default);
    ((NotificationManager)GetSystemService(NotificationService)!).CreateNotificationChannel(channel);
}

}

4.5 PushNotificationFirebaseMessagingService.cs

namespace YOUR_NAMESPACE.Platforms.Android;

using global::Android.App; using Firebase.Messaging;

[Service(Exported = false)] [IntentFilter(new[] { "com.google.firebase.MESSAGING_EVENT" })] public class PushNotificationFirebaseMessagingService : FirebaseMessagingService { public override void OnNewToken(string token) { // GOTCHA: tokens regenerate frequently during debug builds — always re-register. var svc = IPlatformApplication.Current?.Services.GetService<IPushNotificationService>(); if (svc is null) return; svc.Token = token; _ = svc.RegisterAsync(); }

public override void OnMessageReceived(RemoteMessage message)
{
    var n = message.GetNotification();
    if (n is null) return;
    var intent = new global::Android.Content.Intent(this, typeof(MainActivity));
    intent.AddFlags(global::Android.Content.ActivityFlags.ClearTop);
    var pending = PendingIntent.GetActivity(this, 0, intent,
        PendingIntentFlags.OneShot | PendingIntentFlags.Immutable);
    var builder = new Notification.Builder(this, "default_channel")
        .SetContentTitle(n.Title ?? "")
        .SetContentText(n.Body ?? "")
        .SetSmallIcon(Resource.Drawable.appiconfg)
        .SetAutoCancel(true)
        .SetContentIntent(pending);
    ((NotificationManager)GetSystemService(NotificationService)!).Notify(0, builder.Build());
}

}

Step 5 — iOS setup

5.1 Apple Developer portal & Entitlements

  • Enable Push Notifications capability for your App ID.

  • Create an APNs Key (.p8 ) and upload to Azure Notification Hub.

  • Add Platforms/iOS/Entitlements.plist :

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

  • In .csproj :

<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))"> <CodesignEntitlements>Platforms\iOS\Entitlements.plist</CodesignEntitlements> </PropertyGroup>

5.2 AppDelegate.cs

namespace YOUR_NAMESPACE.Platforms.iOS;

using Foundation; using UIKit; using UserNotifications;

[Register("AppDelegate")] public class AppDelegate : MauiUIApplicationDelegate { protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();

public override bool FinishedLaunching(UIApplication application, NSDictionary launchOptions)
{
    var result = base.FinishedLaunching(application, launchOptions);
    UNUserNotificationCenter.Current.RequestAuthorization(
        UNAuthorizationOptions.Alert | UNAuthorizationOptions.Badge | UNAuthorizationOptions.Sound,
        (granted, _) =>
        {
            if (granted)
                InvokeOnMainThread(UIApplication.SharedApplication.RegisterForRemoteNotifications);
        });
    return result;
}

[Export("application:didRegisterForRemoteNotificationsWithDeviceToken:")]
public void RegisteredForRemoteNotifications(UIApplication application, NSData deviceToken)
{
    var token = BitConverter.ToString(deviceToken.ToArray()).Replace("-", "").ToLowerInvariant();
    var svc = IPlatformApplication.Current?.Services.GetService&#x3C;IPushNotificationService>();
    if (svc is null) return;
    svc.Token = token;
    _ = svc.RegisterAsync();
}

[Export("application:didReceiveRemoteNotification:fetchCompletionHandler:")]
public void ReceivedRemoteNotification(UIApplication application,
    NSDictionary userInfo, Action&#x3C;UIBackgroundFetchResult> completionHandler)
{
    completionHandler(UIBackgroundFetchResult.NewData);
}

}

Step 6 — Test the pipeline

  • Run the backend locally or deploy to Azure App Service.

  • Android: deploy to device or emulator with Google Play Services.

  • iOS: deploy to a physical device — simulators cannot receive APNS push notifications.

  • Send a test notification:

curl -X POST https://<your-backend>/api/notifications/requests
-H "Content-Type: application/json"
-H "apikey: <your-api-key>"
-d '{"title":"Hello","body":"Push works!","tags":["user:default-user"]}'

Gotchas and troubleshooting

Issue Cause Fix

Token changes every debug run (Android) Debug builds regenerate Firebase tokens Re-register on every OnNewToken — handled above

HttpClient requests fail silently BaseAddress missing trailing /

Ensure endpoint ends with /

iOS push won't arrive on simulator Simulators don't support APNS Use a physical iOS device

No notifications on Android 13+ POST_NOTIFICATIONS permission required (API 33+) Call RequestPermissions in OnCreate

Notification channel missing (API 26+) Android requires explicit channel creation Create channel before sending

SendNotificationAsync throws for >20 tags Azure NH tag expression limit Batch tags in groups of 20

422 on registration Platform string mismatch Use "fcmv1" (not "gcm" ) and "apns"

Token empty at RegisterAsync

Race condition on cold start Guard with IsNullOrWhiteSpace check

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

maui-performance

No summary provided by upstream source.

Repository SourceNeeds Review
General

maui-data-binding

No summary provided by upstream source.

Repository SourceNeeds Review
General

maui-dependency-injection

No summary provided by upstream source.

Repository SourceNeeds Review
General

maui-permissions

No summary provided by upstream source.

Repository SourceNeeds Review