Skip to content

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 ​

csharp
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:

csharp
HermesApplication.ConfigureNotifications(new NativeNotificationOptions
{
    AppId = "Mythetech.Horizon",
    DisplayName = "Horizon",
    IconPath = Path.Combine(AppContext.BaseDirectory, "icon.png"),
});
OptionDefaultUsed by
AppIdEntry assembly nameWindows AppUserModelID, Linux app name
DisplayNameSame as AppIdWindows notification settings
IconPathNoneWindows 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.

csharp
var notification = new NativeNotification
{
    Title = "New message",
    Body = "Alice: are you around?",
    Tag = "chat/alice",
    IconPath = iconPath,
    Silent = false,
};

await HermesApplication.Notifications.ShowAsync(notification);
PropertyPurpose
IdIdentifies the notification for Dismiss and in Clicked. Generated when omitted
TitleRequired headline
BodyOptional second line
TagOpaque string handed back on click. Use it for routing
IconPathLocal image shown on the notification, where the platform supports it
SilentSuppresses 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.

csharp
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:

csharp
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 ​

csharp
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.

csharp
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.

razor
@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 ​

FeaturemacOSWindowsLinux
APIUNUserNotificationCenterWinRT toast notificationsorg.freedesktop.Notifications over D-Bus
RequirementRunning from a .app bundleWindows 10 or laterA notification daemon on the session bus
Permission promptYes, on first useNoNo
IconAttachmentApp logo on the toastimage-path hint, daemon dependent
Click while runningYesYesYes, 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:

csharp
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 ​