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
<PackageReference Include="RepletoryLib.Communication.Push" Version="1.0.0" />
<PackageVersion Include="RepletoryLib.Communication.Push" Version="1.0.0" />
<PackageReference Include="RepletoryLib.Communication.Push" />
paket add RepletoryLib.Communication.Push --version 1.0.0
#r "nuget: RepletoryLib.Communication.Push, 1.0.0"
#:package RepletoryLib.Communication.Push@1.0.0
#addin nuget:?package=RepletoryLib.Communication.Push&version=1.0.0
#tool nuget:?package=RepletoryLib.Communication.Push&version=1.0.0
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.
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 --
CompositePushServiceroutes notifications to the correct provider based onPushProvider - 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
PushOptionsflags - Unified routing -- Automatically registers
ICommunicationServicefor 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 | Versions 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. |
-
net10.0
- FirebaseAdmin (>= 3.1.0)
- Lib.Net.Http.WebPush (>= 3.3.1)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- RepletoryLib.Communication.Abstractions (>= 1.0.0)
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 |