Native Notifications
Hermes posts native system notifications on every platform and tells your app when the user clicks one. Each notification carries an app-supplied Tag, so a click can navigate straight to the page it belongs to. Nothing new is installed: macOS uses UNUserNotificationCenter, Windows uses toast notifications, and Linux talks to the desktop's notification daemon over D-Bus.
Quick Reference
using Hermes;
using Hermes.Contracts.Notifications;
var notifications = HermesApplication.Notifications;
notifications.Clicked += args =>
{
// args.Id is the notification id, args.Tag is whatever you supplied
Navigate(args.Tag);
};
await notifications.ShowAsync(new NativeNotification
{
Title = "Build finished",
Body = "3 warnings, 0 errors",
Tag = "page/build-log",
});Configuring the App Identity
Windows and macOS attach every notification to an application identity. Call ConfigureNotifications once, before the first notification, to control how the app appears in the system's notification settings:
HermesApplication.ConfigureNotifications(new NativeNotificationOptions
{
AppId = "Mythetech.Horizon",
DisplayName = "Horizon",
IconPath = Path.Combine(AppContext.BaseDirectory, "icon.png"),
});| Option | Default | Used by |
|---|---|---|
AppId | Entry assembly name | Windows AppUserModelID, Linux app name |
DisplayName | Same as AppId | Windows notification settings |
IconPath | None | Windows app logo on the toast |
macOS ignores these values and uses the app bundle's identity instead.
Showing a Notification
NativeNotification is the one type you build. Only Title is required.
var notification = new NativeNotification
{
Title = "New message",
Body = "Alice: are you around?",
Tag = "chat/alice",
IconPath = iconPath,
Silent = false,
};
await HermesApplication.Notifications.ShowAsync(notification);| Property | Purpose |
|---|---|
Id | Identifies the notification for Dismiss and in Clicked. Generated when omitted |
Title | Required headline |
Body | Optional second line |
Tag | Opaque string handed back on click. Use it for routing |
IconPath | Local image shown on the notification, where the platform supports it |
Silent | Suppresses the notification sound |
Showing a notification with an Id that is already on screen replaces the earlier one on Windows and macOS.
Handling Clicks
Subscribe to Clicked to learn when the user activates a notification. The event is raised on the UI thread on every platform, so it is safe to touch windows and application state directly.
HermesApplication.Notifications.Clicked += args =>
{
if (args.Tag is not null)
mainWindow.Navigate(args.Tag);
};NotificationClickedEventArgs has two members: Id and Tag. Each subscriber is invoked separately, so one handler throwing does not stop the others. Exceptions from handlers are routed to HermesApplication.DispatcherUnhandledException.
Clicks are delivered only while the app is running
If the user clicks a notification after the process has exited, no platform delivers the click to a later launch. The system opens or focuses the app at best.
Permission
macOS asks the user for permission before the first notification is shown. Hermes requests it automatically on the first ShowAsync, but asking at a moment of your choosing gives a better prompt:
bool granted = await HermesApplication.Notifications.RequestPermissionAsync();Windows and Linux do not prompt; RequestPermissionAsync returns true whenever notifications are supported. A denied request is logged once and later ShowAsync calls become no-ops.
Dismissing
HermesApplication.Notifications.Dismiss(notification.Id);
HermesApplication.Notifications.DismissAll();Dismiss removes a single notification from the screen and the notification center. DismissAll removes every notification the app has posted.
Unsupported Hosts
Some hosts cannot notify at all: a macOS process started with dotnet run has no bundle identifier, a Linux session may have no notification daemon, and CI runners have neither. Hermes never throws for this case. Check IsSupported when you want to adapt the UI, or ignore it and let ShowAsync log once and return.
var notifications = HermesApplication.Notifications;
if (!notifications.IsSupported)
Console.WriteLine($"Notifications unavailable: {notifications.UnsupportedReason}");Threading
Access HermesApplication.Notifications for the first time on the UI thread: in Program.cs before Run(), or from any UI callback. Blazor components are already on the UI thread. ShowAsync may be awaited from any thread and may complete on any thread, like any other task. Clicked is always raised on the UI thread.
TIP
If a synchronous UI callback needs to show a notification, write the request to a Channel<T> and await ShowAsync from a consumer task instead of blocking the callback.
Blazor Example
Hermes.Blazor registers INativeNotifications in the DI container, so components inject it rather than using the static facade. Subscribe in OnInitialized and unsubscribe in Dispose.
@using Hermes.Contracts.Notifications
@using Hermes.Contracts.Plugins
@inject INativeNotifications Notifications
@inject NavigationManager Navigation
@implements IDisposable
<button @onclick="Notify">Notify me when done</button>
@code {
protected override void OnInitialized()
{
Notifications.Clicked += OnNotificationClicked;
}
private async Task Notify()
{
await Notifications.ShowAsync(new NativeNotification
{
Title = "Export complete",
Body = "report.pdf is ready",
Tag = "/exports",
});
}
private void OnNotificationClicked(NotificationClickedEventArgs args)
{
if (args.Tag is not null)
Navigation.NavigateTo(args.Tag);
}
public void Dispose()
{
Notifications.Clicked -= OnNotificationClicked;
}
}INativeNotifications mirrors the static center exactly, which also makes it easy to substitute in component tests.
Platform Details
| Feature | macOS | Windows | Linux |
|---|---|---|---|
| API | UNUserNotificationCenter | WinRT toast notifications | org.freedesktop.Notifications over D-Bus |
| Requirement | Running from a .app bundle | Windows 10 or later | A notification daemon on the session bus |
| Permission prompt | Yes, on first use | No | No |
| Icon | Attachment | App logo on the toast | image-path hint, daemon dependent |
| Click while running | Yes | Yes | Yes, if the daemon supports actions |
macOS
Notifications require a bundle identifier, so a process started with dotnet run reports IsSupported == false with the reason "process is not running from an app bundle". Publish and bundle the app to test them; the NotificationsDemo sample ships a bundle-macos.sh script that does this. Notifications are presented even while the app is frontmost.
Windows
On first use Hermes registers the app under HKCU\Software\Classes\AppUserModelId\<AppId> with the display name and icon, which is what lets an unpackaged executable post toasts. No package identity, Start menu shortcut, or COM activator is needed. Toasts appear in the Action Center under DisplayName.
Linux
GNOME, KDE Plasma, dunst, and mako all deliver clicks. Minimal daemons that do not advertise the actions capability show the notification but never report a click. When the session bus or the daemon cannot be reached, IsSupported is false and UnsupportedReason carries the D-Bus error.
Testing
Components and services that depend on INativeNotifications can be tested with a substitute:
var notifications = Substitute.For<INativeNotifications>();
notifications.IsSupported.Returns(true);
Services.AddSingleton(notifications);
// ... render the component and click the button ...
await notifications.Received(1).ShowAsync(
Arg.Is<NativeNotification>(n => n.Tag == "/exports"),
Arg.Any<CancellationToken>());To simulate a click, capture the handler the component subscribed and invoke it with a NotificationClickedEventArgs.
Next Steps
- Tray Applications for background apps that notify from the tray
- Platform Notes for the full cross-platform feature matrix
