RepletoryLib.Communication.Push 1.0.0

dotnet add package RepletoryLib.Communication.Push --version 1.0.0
                    
NuGet\Install-Package RepletoryLib.Communication.Push -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="RepletoryLib.Communication.Push" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="RepletoryLib.Communication.Push" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="RepletoryLib.Communication.Push" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add RepletoryLib.Communication.Push --version 1.0.0
                    
#r "nuget: RepletoryLib.Communication.Push, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package RepletoryLib.Communication.Push@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=RepletoryLib.Communication.Push&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=RepletoryLib.Communication.Push&version=1.0.0
                    
Install as a Cake Tool

RepletoryLib.Communication.Push

Multi-provider push notification service supporting Firebase Cloud Messaging, Expo Push, and Web Push (VAPID).

Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.

NuGet .NET 10 License: MIT


Overview

RepletoryLib.Communication.Push provides a multi-provider implementation of the IPushNotificationService interface from RepletoryLib.Communication.Abstractions. It supports three push notification providers -- Firebase Cloud Messaging (FCM), Expo Push Notifications, and Web Push (VAPID) -- through a composite routing pattern that directs each notification to the correct provider based on the PushProvider enum.

Each provider can be independently enabled or disabled via configuration. The CompositePushService acts as the IPushNotificationService implementation registered in DI, routing notifications to the appropriate provider-specific service. Bulk sends are automatically grouped by provider for efficient batching.

Key Features

  • Firebase Cloud Messaging -- Send to Android, iOS, and web via the Firebase Admin SDK with platform-specific config (Android channels, APNs badges/sounds)
  • Expo Push -- Send to Expo-managed apps via the Expo Push API with native bulk support
  • Web Push (VAPID) -- Send to browser endpoints using the VAPID protocol via Lib.Net.Http.WebPush
  • Composite routing -- CompositePushService routes notifications to the correct provider based on PushProvider
  • Device targeting -- Send to specific devices via tokens
  • Topic targeting -- Send to topic subscribers (Firebase only)
  • Bulk send -- Send multiple notifications with per-message results, automatically grouped by provider
  • Platform config -- Android notification channels, APNs badge/sound, rich image URLs
  • Selective enablement -- Enable only the providers you need via PushOptions flags
  • Unified routing -- Automatically registers ICommunicationService for cross-channel messaging

Installation

dotnet add package RepletoryLib.Communication.Push

Or add to your .csproj:

<PackageReference Include="RepletoryLib.Communication.Push" Version="1.0.0" />

Note: RepletoryLib packages are published to a local BaGet feed. See the main repository README for feed configuration.

Dependencies

Package Type
RepletoryLib.Communication.Abstractions RepletoryLib
FirebaseAdmin NuGet (3.1.0)
Lib.Net.Http.WebPush NuGet (3.3.1)
Microsoft.Extensions.Http NuGet (10.0.0)
Microsoft.Extensions.Configuration.Binder NuGet (10.0.0)
Microsoft.Extensions.DependencyInjection.Abstractions NuGet (10.0.0)
Microsoft.Extensions.Logging.Abstractions NuGet (10.0.0)
Microsoft.Extensions.Options.ConfigurationExtensions NuGet (10.0.0)

Prerequisites

  • Firebase: Service account credential JSON file or inline JSON string
  • Expo: Expo Push access token (optional, for authenticated requests)
  • Web Push: VAPID public/private key pair and subject URL

Quick Start

using RepletoryLib.Communication.Push;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRepletoryPush(builder.Configuration);
{
  "Communication": {
    "Push": {
      "EnableFirebase": true,
      "EnableExpo": true,
      "EnableWebPush": true,
      "Firebase": {
        "CredentialPath": "/path/to/service-account.json",
        "ProjectId": "my-firebase-project"
      },
      "Expo": {
        "AccessToken": "your-expo-access-token",
        "BaseUrl": "https://exp.host/--/api/v2/push/send"
      },
      "WebPush": {
        "VapidPublicKey": "BPub...",
        "VapidPrivateKey": "priv...",
        "VapidSubject": "mailto:admin@example.com"
      }
    }
  }
}

Configuration

PushOptions

Property Type Default Description
EnableFirebase bool false Enable Firebase Cloud Messaging provider
EnableExpo bool false Enable Expo Push Notification provider
EnableWebPush bool false Enable Web Push (VAPID) provider

Section name: "Communication:Push"

FirebaseOptions

Property Type Default Description
CredentialPath string? null File path to Firebase service account credential JSON. Mutually exclusive with CredentialJson
CredentialJson string? null Inline Firebase service account credential JSON string. Mutually exclusive with CredentialPath
ProjectId string? null Firebase project ID. When null, inferred from the credential

Section name: "Communication:Push:Firebase"

ExpoOptions

Property Type Default Description
AccessToken string? null Expo access token for authenticated push requests. When null, requests are unauthenticated
BaseUrl string "https://exp.host/--/api/v2/push/send" Expo Push API base URL

Section name: "Communication:Push:Expo"

WebPushOptions

Property Type Default Description
VapidPublicKey string "" VAPID public key for push subscription authentication
VapidPrivateKey string "" VAPID private key for signing push messages
VapidSubject string "" VAPID subject (typically a mailto: URL) identifying the application server

Section name: "Communication:Push:WebPush"


Usage Examples

Register Services

using RepletoryLib.Communication.Push;

var builder = WebApplication.CreateBuilder(args);

// Register push notification services (providers enabled via config)
builder.Services.AddRepletoryPush(builder.Configuration);

var app = builder.Build();

Send via Firebase

using RepletoryLib.Communication.Abstractions.Enums;
using RepletoryLib.Communication.Abstractions.Interfaces;
using RepletoryLib.Communication.Abstractions.Models.Push;

public class MobilePushService
{
    private readonly IPushNotificationService _pushService;

    public MobilePushService(IPushNotificationService pushService) => _pushService = pushService;

    public async Task SendToAndroidAsync(string fcmToken, string title, string body)
    {
        var result = await _pushService.SendToDeviceAsync(new PushNotification
        {
            DeviceToken = fcmToken,
            Title = title,
            Body = body,
            Provider = PushProvider.Firebase,
            ChannelId = "default-channel",
            Sound = "notification.mp3",
            Badge = 1,
            Data = new Dictionary<string, string>
            {
                ["orderId"] = "ORD-2024-001",
                ["action"] = "open_order"
            }
        });

        if (!result.Success)
            throw new InvalidOperationException($"Push failed: {result.Error}");
    }
}

Send via Firebase to a Topic

var result = await _pushService.SendToTopicAsync(new TopicPushNotification
{
    Topic = "breaking-news",
    Title = "Breaking News",
    Body = "Major event reported in downtown area.",
    Provider = PushProvider.Firebase,
    Data = new Dictionary<string, string>
    {
        ["category"] = "news",
        ["articleId"] = "ART-2024-500"
    }
});

Send via Expo

var result = await _pushService.SendToDeviceAsync(new PushNotification
{
    DeviceToken = "ExponentPushToken[xxxxxxxxxxxxxxxx]",
    Title = "New Message",
    Body = "You have a new message from John.",
    Provider = PushProvider.Expo,
    Badge = 3,
    Sound = "default",
    ChannelId = "messages",
    Data = new Dictionary<string, string>
    {
        ["chatId"] = "chat-456",
        ["senderId"] = "user-789"
    }
});

Send via Web Push

// Web Push expects the DeviceToken to be a JSON-serialized PushSubscription
var subscriptionJson = """
{
    "endpoint": "https://fcm.googleapis.com/fcm/send/...",
    "keys": {
        "p256dh": "BPub...",
        "auth": "auth..."
    }
}
""";

var result = await _pushService.SendToDeviceAsync(new PushNotification
{
    DeviceToken = subscriptionJson,
    Title = "New Update Available",
    Body = "Version 2.0 is now available. Click to update.",
    Provider = PushProvider.WebPush,
    ImageUrl = "https://cdn.example.com/icons/update.png",
    Badge = 1
});

Rich Notification with Image

var result = await _pushService.SendToDeviceAsync(new PushNotification
{
    DeviceToken = fcmToken,
    Title = "Your Order Has Shipped!",
    Body = "Track your package with tracking number XYZ123.",
    Provider = PushProvider.Firebase,
    ImageUrl = "https://cdn.example.com/shipping-banner.jpg",
    Badge = 0,
    Sound = "shipped.mp3",
    Data = new Dictionary<string, string>
    {
        ["trackingUrl"] = "https://tracking.example.com/XYZ123"
    }
});

Bulk Send (Multi-Provider)

var notifications = new List<PushNotification>
{
    new()
    {
        DeviceToken = "fcm-token-1",
        Title = "Sale Alert",
        Body = "50% off all items!",
        Provider = PushProvider.Firebase
    },
    new()
    {
        DeviceToken = "ExponentPushToken[xxx]",
        Title = "Sale Alert",
        Body = "50% off all items!",
        Provider = PushProvider.Expo
    },
    new()
    {
        DeviceToken = webPushSubscriptionJson,
        Title = "Sale Alert",
        Body = "50% off all items!",
        Provider = PushProvider.WebPush
    }
};

// CompositePushService groups by provider and delegates to each
var bulkResult = await _pushService.SendBulkAsync(notifications);

Console.WriteLine($"Sent: {bulkResult.SuccessCount}, Failed: {bulkResult.FailureCount}");

Using the Unified Communication Facade

using RepletoryLib.Communication.Abstractions.Enums;
using RepletoryLib.Communication.Abstractions.Interfaces;
using RepletoryLib.Communication.Abstractions.Models.Common;

public class AlertService
{
    private readonly ICommunicationService _communication;

    public AlertService(ICommunicationService communication) => _communication = communication;

    public async Task SendPushAlertAsync(string deviceToken, string title, string body)
    {
        // Routes through CommunicationService -> CompositePushService -> Firebase (default provider)
        var result = await _communication.SendAsync(new CommunicationMessage
        {
            Channel = CommunicationChannel.Push,
            Recipient = deviceToken,
            Subject = title,
            Body = body,
            Metadata = new Dictionary<string, string> { ["action"] = "open_alerts" }
        });
    }
}

Multi-Channel Registration

using RepletoryLib.Communication.Email;
using RepletoryLib.Communication.Sms;
using RepletoryLib.Communication.WhatsApp;
using RepletoryLib.Communication.Push;

var builder = WebApplication.CreateBuilder(args);

// Register all communication channels
builder.Services.AddRepletoryEmail(builder.Configuration);
builder.Services.AddRepletrySms(builder.Configuration);
builder.Services.AddRepletoryWhatsApp(builder.Configuration);
builder.Services.AddRepletoryPush(builder.Configuration);

API Reference

ServiceCollectionExtensions

Method Returns Description
AddRepletoryPush(configuration) IServiceCollection Registers enabled push providers, IPushNotificationService (scoped via CompositePushService), and ICommunicationService (scoped)

CompositePushService (implements IPushNotificationService)

Method Returns Description
SendToDeviceAsync(PushNotification, ct) Task<SendResult> Routes to the correct provider and sends to a specific device
SendToTopicAsync(TopicPushNotification, ct) Task<SendResult> Routes to the correct provider and sends to a topic
SendBulkAsync(IEnumerable<PushNotification>, ct) Task<BulkSendResult> Groups by provider and delegates bulk send to each

FirebasePushService (implements IPushNotificationService)

Method Returns Description
SendToDeviceAsync(PushNotification, ct) Task<SendResult> Sends via Firebase Admin SDK with Android/APNs config
SendToTopicAsync(TopicPushNotification, ct) Task<SendResult> Sends to a Firebase topic
SendBulkAsync(IEnumerable<PushNotification>, ct) Task<BulkSendResult> Sends multiple notifications sequentially

ExpoPushService (implements IPushNotificationService)

Method Returns Description
SendToDeviceAsync(PushNotification, ct) Task<SendResult> Sends via the Expo Push API
SendToTopicAsync(TopicPushNotification, ct) Task<SendResult> Returns failure -- Expo does not support topic-based notifications
SendBulkAsync(IEnumerable<PushNotification>, ct) Task<BulkSendResult> Native Expo bulk API with per-ticket results

WebPushService (implements IPushNotificationService)

Method Returns Description
SendToDeviceAsync(PushNotification, ct) Task<SendResult> Sends via VAPID protocol. DeviceToken must be a JSON-serialized PushSubscription
SendToTopicAsync(TopicPushNotification, ct) Task<SendResult> Returns failure -- Web Push does not support topic-based notifications
SendBulkAsync(IEnumerable<PushNotification>, ct) Task<BulkSendResult> Sends multiple notifications sequentially

Provider Feature Matrix

Feature Firebase Expo Web Push
Device targeting Yes Yes Yes
Topic targeting Yes No No
Image URL Yes No Yes (as icon)
Badge count Yes Yes Yes
Sound Yes Yes (default) No
Android channel Yes Yes No
Data payload Yes Yes Yes
Native bulk API No Yes No

Integration with Other RepletoryLib Packages

Package Relationship
RepletoryLib.Communication.Abstractions Direct dependency -- provides IPushNotificationService, PushNotification, SendResult
RepletoryLib.Communication.Email Combine with email for multi-channel notifications
RepletoryLib.Communication.Sms Combine with SMS for multi-channel notifications
RepletoryLib.Communication.WhatsApp Combine with WhatsApp for multi-channel notifications
RepletoryLib.Common Transitive dependency via Abstractions

Testing

using Moq;
using Microsoft.Extensions.Logging.Abstractions;
using RepletoryLib.Communication.Abstractions.Enums;
using RepletoryLib.Communication.Abstractions.Models.Common;
using RepletoryLib.Communication.Abstractions.Models.Push;
using RepletoryLib.Communication.Push.Services;

[Fact]
public async Task CompositePushService_routes_to_correct_provider()
{
    var mockFirebase = new Mock<FirebasePushService>(NullLogger<FirebasePushService>.Instance);
    mockFirebase
        .Setup(f => f.SendToDeviceAsync(It.IsAny<PushNotification>(), It.IsAny<CancellationToken>()))
        .ReturnsAsync(SendResult.Succeeded(CommunicationChannel.Push, "fcm-msg-001"));

    var composite = new CompositePushService(firebase: mockFirebase.Object);

    var result = await composite.SendToDeviceAsync(new PushNotification
    {
        DeviceToken = "fcm-token-123",
        Title = "Test",
        Body = "Hello",
        Provider = PushProvider.Firebase
    });

    result.Success.Should().BeTrue();
    result.MessageId.Should().Be("fcm-msg-001");
}

[Fact]
public async Task CompositePushService_returns_failure_for_disabled_provider()
{
    var composite = new CompositePushService(); // No providers registered

    var result = await composite.SendToDeviceAsync(new PushNotification
    {
        DeviceToken = "token-123",
        Title = "Test",
        Body = "Hello",
        Provider = PushProvider.Expo
    });

    result.Success.Should().BeFalse();
    result.Error.Should().Contain("not enabled or registered");
}

Troubleshooting

Issue Solution
FirebaseMessagingException with UNREGISTERED The device token is no longer valid. Remove it from your database and stop sending to it
FirebaseMessagingException with INVALID_ARGUMENT Check the notification payload -- title and body must not be empty
Firebase DefaultInstance is null Ensure EnableFirebase is true in config and the credential path/JSON is valid
Expo returns DeviceNotRegistered The Expo push token is invalid. Remove it from your database
Expo topic-based send fails Expo does not support topic-based notifications. Use device tokens instead
Web Push JsonException The DeviceToken must be a valid JSON-serialized push subscription with endpoint and keys
Web Push PushServiceClientException Check the subscription endpoint URL and ensure VAPID keys are correct
Web Push topic-based send fails Web Push does not support topic-based notifications. Use individual subscriptions
SendResult.Error says "provider is not enabled" Set the corresponding Enable* flag to true in the Communication:Push config section
SendResult.Error says "Push notification service is not registered" Ensure AddRepletoryPush(configuration) is called in Program.cs

License

This project is licensed under the MIT License.

Copyright (c) 2024-2026 Repletory.


For complete documentation, infrastructure setup, and configuration reference, see the RepletoryLib main repository.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 64 3/2/2026