# Notify.NET A cross-platform .NET Standard library for displaying OS notifications. Provides a single fluent API that dispatches to the native notification system on each supported platform. | Platform | Backend | Minimum OS | |----------|---------|------------| | Windows | [WinToastLib](https://github.com/mohabouje/WinToast) via a thin C++ wrapper DLL | Windows 8 | | Linux | libnotify via P/Invoke | Any distribution with a D-Bus notification daemon | | macOS | UNUserNotificationCenter via a thin Objective-C wrapper dylib | macOS 10.14 (Mojave) | --- ## Installation ``` dotnet add package Notify.NET ``` The NuGet package includes the pre-compiled native libraries for all supported platforms. No separate native installation is required. --- ## Quick start ```csharp using Notify.NET.Builder; using Notify.NET.Extensions; using var service = ServiceCollectionExtensions.CreateNotificationService(opts => { opts.AppName = "My App"; opts.AppUserModelId = "MyCompany.MyApp"; // Windows only; ignored on other platforms }); if (service.IsSupported) { long id = await service.ShowAsync( NotificationBuilder.Create("Hello") .WithBody("Notify.NET is working.") .Build()); } ``` --- ## Creating the service ### Without a DI container ```csharp using var service = ServiceCollectionExtensions.CreateNotificationService(opts => { opts.AppName = "My App"; opts.AppUserModelId = "MyCompany.MyApp"; }); ``` `CreateNotificationService` selects the correct implementation for the current OS automatically. On unsupported platforms it returns a no-op service where `IsSupported` is `false`. ### With Microsoft.Extensions.DependencyInjection ```csharp services.AddNotifications(opts => { opts.AppName = "My App"; opts.AppUserModelId = "MyCompany.MyApp"; }); ``` This registers `INotificationService` as a singleton. Resolve it normally: ```csharp var service = provider.GetRequiredService(); ``` ### Checking platform support Always check `IsSupported` before calling `ShowAsync`. On unsupported platforms or when initialisation fails (e.g. the user denied notification permission on macOS), `ShowAsync` throws `PlatformNotSupportedException`. ```csharp if (!service.IsSupported) { Console.WriteLine("Notifications are not available on this platform."); return; } ``` --- ## Builder API All notification configuration is done through `NotificationBuilder`. The `Build()` call produces an immutable `NotificationRequest` that can be passed to `ShowAsync`. ### Title and body ```csharp var request = NotificationBuilder.Create("Title goes here") .WithBody("Optional body text goes here.") .Build(); ``` ### Image Pass an absolute path to an image file. Relative paths are resolved to absolute paths at call time; if the file does not exist the notification is shown without an image rather than failing. ```csharp var request = NotificationBuilder.Create("New photo") .WithBody("A photo has arrived.") .WithImage("/home/user/photos/latest.jpg") .Build(); ``` Supported formats depend on the platform (PNG and JPEG work on all three). ### Action buttons Up to five action buttons can be added. Each button has a label and an optional click callback that receives the notification ID. ```csharp var request = NotificationBuilder.Create("Update available") .WithBody("Version 2.0 is ready.") .AddButton("Install now", id => Installer.Run()) .AddButton("Remind me later", id => ScheduleReminder()) .AddButton("Skip this version", null) .Build(); ``` ### Lifecycle callbacks Register delegates for specific notification events: ```csharp var request = NotificationBuilder.Create("Download complete") .WithBody("report.pdf has been saved.") .OnActivated(id => OpenFile("report.pdf")) .OnDismissed((id, reason) => { if (reason == DismissReason.UserCancelled) Console.WriteLine("User dismissed the notification."); }) .OnFailed(id => Console.WriteLine($"Notification {id} could not be displayed.")) .Build(); ``` For more complex cases, implement `INotificationHandler` and attach it with `WithHandler`: ```csharp public sealed class DownloadHandler : INotificationHandler { public void OnActivated(long id) => OpenDownloadFolder(); public void OnButtonActivated(long id, int i) => HandleButton(i); public void OnDismissed(long id, DismissReason r) { } public void OnFailed(long id) => LogError(id); } var request = NotificationBuilder.Create("Download complete") .WithHandler(new DownloadHandler()) .Build(); ``` `WithHandler` takes precedence over any delegate callbacks registered on the same builder. ### Urgency ```csharp // Low priority — the platform may suppress or delay it NotificationBuilder.Create("FYI").WithUrgency(NotificationUrgency.Low) // Critical — bypasses Do Not Disturb where the platform supports it NotificationBuilder.Create("Disk full").WithUrgency(NotificationUrgency.Critical) // Windows-specific scenarios NotificationBuilder.Create("Meeting in 5 minutes").WithUrgency(NotificationUrgency.Reminder) NotificationBuilder.Create("Incoming call").WithUrgency(NotificationUrgency.Alarm) ``` | Value | Windows | Linux | macOS | |-------|---------|-------|-------| | `Normal` | Default scenario | Normal urgency | Active interruption level | | `Low` | Default scenario | Low urgency | Passive interruption level | | `Critical` | Default scenario | Critical urgency | Critical interruption level (macOS 12+) | | `Alarm` | Alarm scenario | Critical urgency | Critical interruption level (macOS 12+) | | `Reminder` | Reminder scenario | Normal urgency | Active interruption level | ### Audio ```csharp NotificationBuilder.Create("Alert").WithAudio(NotificationAudio.Silent) NotificationBuilder.Create("Alarm").WithAudio(NotificationAudio.Loop) // Windows only ``` | Value | Windows | Linux | macOS | |-------|---------|-------|-------| | `Default` | System notification sound | Controlled by daemon | System notification sound | | `Silent` | No sound | No sound | No sound | | `Loop` | Sound loops until dismissed | Treated as default | Treated as default | ### Expiration Sets how long the notification is visible before it auto-dismisses. Pass `null` or omit the call to use the platform default. ```csharp NotificationBuilder.Create("Reminder") .WithExpiration(TimeSpan.FromSeconds(10)) .Build(); ``` Note: `UNUserNotificationCenter` on macOS does not expose a per-notification timeout API; this value is stored in the request but has no effect on macOS. --- ## Showing and hiding notifications `ShowAsync` returns a `long` identifier for the notification. Pass this to `HideAsync` to remove it programmatically before the user interacts with it. ```csharp long id = await service.ShowAsync(request); // Remove it after two seconds await Task.Delay(TimeSpan.FromSeconds(2)); await service.HideAsync(id); ``` --- ## Callback threading Callbacks are fired on a background thread: - **Windows** — callbacks arrive on a WinRT thread-pool thread. - **Linux** — callbacks arrive on the GLib main loop thread. - **macOS** — callbacks arrive on a background GCD thread managed by `UNUserNotificationCenter`. If you need to update UI elements from a callback, marshal the call to your UI thread (e.g. `Dispatcher.InvokeAsync` on WPF, `Control.Invoke` on WinForms, or `MainThread.BeginInvokeOnMainThread` on MAUI). --- ## INotificationService interface ```csharp public interface INotificationService : IDisposable { // False if the platform is unsupported or initialisation failed. bool IsSupported { get; } // Shows a notification. Returns the notification ID on success. // Throws PlatformNotSupportedException if IsSupported is false. Task ShowAsync(NotificationRequest request, CancellationToken cancellationToken = default); // Removes the notification from the notification centre. Task HideAsync(long notificationId, CancellationToken cancellationToken = default); } ``` Dispose the service when your application exits to release the native resources (WinToastLib STA thread on Windows, GLib main loop on Linux, UNUserNotificationCenter cleanup on macOS). --- ## Platform notes ### Windows - Requires Windows 8 or later. `IsSupported` returns `false` on earlier versions. - The `AppUserModelId` must match an application shortcut in the Start Menu. The native wrapper creates this shortcut automatically on first run when it does not already exist, but the shortcut creation requires that the process can write to the user's `%APPDATA%\Microsoft\Windows\Start Menu\Programs` folder. - `WinToastWrapper.dll` is loaded at runtime from `runtimes/win-/native/` relative to the entry assembly. For published single-file applications, ensure the DLL is published alongside the executable. - Toast callbacks are delivered on a WinRT thread-pool thread, not the STA thread. The library handles this internally. ### Linux `libnotify` must be installed at runtime. Install it via your package manager: ``` # Debian / Ubuntu sudo apt install libnotify4 # Fedora / RHEL sudo dnf install libnotify # Arch sudo pacman -S libnotify ``` A running D-Bus notification daemon is required (GNOME Shell, KDE Plasma, and most other desktop environments provide one). Notifications will be silently dropped if no daemon is running. `IsSupported` reflects whether `notify_init()` succeeded, not whether a daemon is present. Image support via `gdk-pixbuf` requires `libgdk-pixbuf-2.0` to be installed, which is typically included as a dependency of `libnotify4`. ### macOS - Requires macOS 10.14 (Mojave) or later. - On first use, macOS presents an authorisation dialog asking the user to allow notifications. `MNW_Initialize` (called from the `MacOSNotificationService` constructor) blocks until the user responds. `IsSupported` is `false` if permission was denied. - The user can revoke permission at any time in System Settings > Notifications. Subsequent calls to `ShowAsync` will fail silently (the OS will not display the notification and no callback fires) until permission is re-granted. - Action button callbacks and dismiss callbacks require the process to remain running after the notification is shown, because `UNUserNotificationCenterDelegate` delivers responses to the live process. If the process exits before the user interacts, the callbacks are never fired. - Non-bundled processes (bare `dotnet` CLI applications) can display banners but may not always receive action callbacks on all OS versions. Wrap the application as an `.app` bundle or sign it with an appropriate entitlement for reliable callback delivery in production. - The notification body-tap and button-tap events on macOS are terminal events — the `OnDismissed` callback is not fired after the user activates a notification or clicks a button (unlike Windows, where WinToastLib always fires the dismissed event after any interaction). --- ## Building the native libraries from source Pre-built native binaries are included in the NuGet package. You only need to build from source if you are making changes to the native wrapper code. ### Windows (WinToastWrapper.dll) Requires Visual Studio 2022 with the "Desktop development with C++" workload. ``` msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=x64 msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=Win32 msbuild native\WinToastWrapper\WinToastWrapper.vcxproj /p:Configuration=Release /p:Platform=ARM64 ``` Output is written to `runtimes\win-\native\WinToastWrapper.dll`. ### macOS (libMacNotifyWrapper.dylib) Requires Xcode command-line tools (`xcode-select --install`). ``` make -C native/MacNotifyWrapper install ``` This cross-compiles both an `arm64` (Apple Silicon, macOS 11+) and an `x86_64` (Intel, macOS 10.14+) slice and copies them to `runtimes/osx-arm64/native/` and `runtimes/osx-x64/native/` respectively. To also produce a universal binary: ``` make -C native/MacNotifyWrapper universal ``` --- ## License MIT. See [LICENSE](LICENSE) for details. WinToastLib is copyright (c) mohabouje, also distributed under the MIT License.