A Vapid WebPush client for .NET.
Find a file
Byrone 418ee6beff
All checks were successful
Publish package / publish (push) Successful in 1m22s
Moved repo
2026-03-31 11:59:01 +02:00
.forgejo/workflows Moved repo 2026-03-31 11:59:01 +02:00
.idea/.idea.Vapid.NET/.idea Moved repo 2026-03-31 11:59:01 +02:00
Vapid.NET Moved repo 2026-03-31 11:59:01 +02:00
.gitignore initial 2025-06-22 12:29:52 +02:00
CHANGELOG.md initial 2025-06-22 12:29:52 +02:00
icon.png Moved repo 2026-03-31 11:59:01 +02:00
LICENSE.md Moved repo 2026-03-31 11:59:01 +02:00
README.md Moved repo 2026-03-31 11:59:01 +02:00
Vapid.NET.slnx Moved repo 2026-03-31 11:59:01 +02:00

CleanUI icon

Vapid.NET

A minimal, no dependency Vapid WebPush client for .NET.

Motivation

The original/most popular WebPush client for .NET has multiple issues (in my opinion):

  • It uses the Portable.BouncyCastle package.
    • This package was made before .NET had built-in Cryptography functions that WebPush requires.
    • There is a pull request that tries to fix this, however, this code uses Windows-only functions and hasn't been updated to resolve this issue.
  • It uses the Newtonsoft.Json package.
    • Again, this package was made before .NET had efficient built-in JSON parsing.
    • The (current) latest master branch has received a commit to remove this package, however, the package hasn't been updated since July 2021.
  • It can allocate quite some data in memory.
  • You are almost required to write a wrapper around this package to make it more developer-friendly.

Features

  • Lightweight package that has little to no dependencies
  • Faster than web-push-csharp when creating HTTP requests.
    • The speed of this library is almost completely dependent on the speed of the WebPush server.
  • Declarative Web Push support.
  • Clean and developer-friendly API.

Missing features

  • No GCM support.
    • Even though GCM support seems redundant, an application might still need it.

Usage

ASP.NET Core: Server

  1. Configure Vapid options and register the Vapid client for DI:
using Vapid.NET;

services.Configure<VapidOptions>((options) =>
{
	// You can generate this information on the following website:
	// https://vapidkeys.com

	options.Subject = "…"; // Most of the time, this is an `mailto:…` address.
	options.PublicKey = "…";
	options.PrivateKey = "…";
});

services.AddHttpClient<VapidClient>("Vapid WebPush");
  1. Send a push notification:
using Vapid.NET;
using Vapid.NET.Models;

// Don't forget to inject the `VapidClient`.

// You'd probably store and fetch this from a database:
var pushSubscription = new PushSubscription
{
	Endpoint = "…",
	P256dh = "…",
	Auth = "…",
};

var notification = new PushNotification
{
	Title = "Vapid.NET",
	Body = "Hello from the server!",
	Navigate = "https://vapid.net/docs",
};

// `SendAsync` returns a boolean indicating if the push notification was sent.
var sent = await client.SendAsync(pushSubscription, notification, /*optional:*/ cancellationToken);

ASP.NET Core: Client

Push notifications are sent using the Declarative Web Push format. But not a lot of browsers support this format (yet). You should make sure your application has a service worker that handles the push event:

self.addEventListener('push', function (event) {
	event.waitUntil(showNotification(event));
});

self.addEventListener('notificationclick', function (event) {
	event.notification.close();

	event.waitUntil(onNotificationClick(event));
});

async function showNotification(event) {
	if (!event.data) {
		throw new Error('Received push event without any data.');
	}

	const data = await event.data.json();
	const notification = data.notification;

	await self.registration.showNotification(notification.title, {
		icon: '/icon-512.png',
		lang: notification.lang,
		dir: notification.dir,
		body: notification.body,
		silent: notification.silent,
		tag: notification.topic,
		data: {
			navigate: notification.navigate,
		}
	});
}

async function onNotificationClick(event) {
	const notification = event.notification;

	const url = notification.data.navigate;

	const windows = await clients.matchAll({type: 'window'});

	for (const client of windows) {
		if (client.url === url && 'focus' in client) {
			return await client.focus();
		}
	}

	if (clients.openWindow) {
		return clients.openWindow(url);
	}
}