Skip to main content

User notifications

Lagoon user notifications enable you to send persistent messages to one or more users, synchronize them in real time with SignalR, and store them on the browser side in IndexedDB to remain accessible offline. The interface is provided by the separate module Lagoon.UI.Notifications, distributed via the NuGet package Lagoon.UI.Notifications.

Verified namespace

Public components of the module are exposed in Lagoon.UI.Components (LgNotificationMenuItem, LgNotificationItem). The simple counter LgNotificationCounter is in Lagoon.UI.Components.Internal. The namespace is therefore not Lagoon.UI.Notifications.Components in the current sources.

Confirmation messages (ShowConfirm) and toastr notifications (ShowInformation, ShowError, ShowWarning, ShowSuccess) remain general features of Lagoon.UI; they do not depend on the persistent notifications module described here.

Prerequisites & installation​

Data model​

In the Model project, create a notification entity that inherits from NotificationBase.

Notification.cs
public class Notification : NotificationBase
{
public string Category { get; set; }
}

NotificationBase provides the fields Id, Title, Description, and CreationDate. The user/notification association is carried by NotificationUserWithUser<Notification, ApplicationUser> for users with string keys, or NotificationUserWithUserGuid<Notification, ApplicationUser> for users with Guid keys; it contains in particular Id, NotificationId, UserId, IsRead, and UpdateDate.

Declare the storage in the application DbContext with UseNotifications<TNotification>.

ApplicationDbContext.cs
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);

// Adds the notifications and user association tables.
UseNotifications<Notification>(builder);
}
Migration

After adding the entity and the call to UseNotifications<Notification>, create an EF Core migration to generate the Notifications and NotificationUsers tables (or the custom names passed to UseNotifications).

Shared ViewModel​

In the Shared project, create a ViewModel that inherits from NotificationVmBase.

NotificationVm.cs
public class NotificationVm : NotificationVmBase
{
public string Category { get; set; }
}

NotificationVmBase contains Title, Description, CreationDate, UpdateDate, TargetId, NotificationId, NotificationUserId, and IsRead. The default sort is descending by CreationDate; override CompareTo in the ViewModel for a different order.

Client-side registration​

Add the NuGet package Lagoon.UI.Notifications to the Client project, then register the notifications service.

Program.cs
builder.Services.AddNotification<NotificationVm>();

This call registers INotificationService<NotificationVm> and INotificationService. It can only be called once in the client application.

Server-side registration​

Create a server manager. Use LgNotificationManager<TUser, TVmNotification, TNotification> for users with string keys, or LgNotificationManagerGuid<TUser, TVmNotification, TNotification> for users with Guid keys.

NotificationManager.cs
public class NotificationManager : LgNotificationManager<ApplicationUser, NotificationVm, Notification>
{
public NotificationManager(ApplicationDbContext db, IHubContext<NotificationHub> hub)
: base(db, hub)
{ }

protected override void MapViewModel(NotificationVm vmNotification, Notification notification)
{
// Copies the business fields added to the entity to the client ViewModel.
vmNotification.Category = notification.Category;
}
}

Register the manager in the server services.

Registry.cs
services.AddNotification<NotificationManager>();

AddNotification<TNotificationManager> activates SignalR, the MessagePack protocol, the user identifier provider, and the NotificationHub. The hub is mapped automatically by MapLagoonFallback when the service is present.

Then expose the synchronization controller.

NotificationController.cs
[ApiController]
public class NotificationController : LgNotificationBaseController<Notification, NotificationVm>
{
public NotificationController(ILgNotificationManager<NotificationVm, Notification> notificationManager)
: base(notificationManager)
{ }
}

Add one or more notification menus to the menu configuration. The recommended component is LgNotificationMenuItem<TItem>.

MenuConfiguration.razor
<LgMenuSet Id="grpNotifications">
<AuthorizeView Policy="@AppPolicies.CanReadNotifications">
<LgNotificationMenuItem TItem="NotificationVm"
Text="#Notifications"
IconName="@IconNames.All.BellFill"
OnNotificationClick="OnNotificationClickAsync"
ItemIconName="GetNotificationIconName"
ItemCssClass="GetNotificationCssClass" />
</AuthorizeView>
</LgMenuSet>
MenuConfiguration.razor.cs
public partial class MenuConfiguration
{
// Selects the icon for each notification based on its business data.
private string GetNotificationIconName(NotificationVm notification)
{
return IconNames.All.BellFill;
}

// Adds a CSS class to enable differentiated rendering by application theme.
private string GetNotificationCssClass(NotificationVm notification)
{
return notification.IsRead ? "notification-read" : "notification-unread";
}

// Reacts to a click on a notification without automatically changing its read state.
private Task OnNotificationClickAsync(NotificationEventArgs<NotificationVm> args)
{
return Task.CompletedTask;
}
}

To display multiple streams, use FilterPredicate. For example, one menu can show only notifications from a given category, while another displays tasks or alerts.

MenuConfiguration.razor
<LgNotificationMenuItem TItem="NotificationVm"
Text="#MailNotifications"
IconName="@IconNames.All.BellFill"
FilterPredicate="IsMailNotification" />
MenuConfiguration.razor.cs
public partial class MenuConfiguration
{
// Keeps only notifications from the mail category.
private bool IsMailNotification(NotificationVm notification)
{
return notification.Category == "mail";
}
}

LgNotificationCounter also exists as a minimal visual counter, but it does not handle loading or synchronization: it only displays the value passed in Counter.

LgNotificationItem is the unit rendering used by LgNotificationMenuItem. It exposes public parameters, but its normal usage is internal to the menu; instead use NotificationTemplate on LgNotificationMenuItem to customize the notification card.

Server-side — send / delete​

The server service to inject is ILgNotificationManager<TVmNotification, TNotification>.

Main signatures​

MethodDescription
Task SendNotificationAsync(TNotification notification, Guid userId)Sends a notification to a user.
Task SendNotificationAsync(TNotification notification, IEnumerable<Guid> userIds)Sends a notification to multiple users.
Task DeleteNotificationAsync(Guid notificationId, Guid? ownerUserId)Deletes a notification and all its user associations.
Task DeleteUserNotificationAsync(Guid notificationUserId, Guid? ownerUserId = null)Deletes the association of a notification for a user.
Task DeleteUserNotificationAsync(List<Guid> notificationUserIds, Guid? ownerUserId = null)Deletes multiple user associations.
Task UpdateNotificationReadStateAsync(Guid notificationUserId, bool isRead, Guid userId)Marks a notification as read or unread for a user.
Task UpdateNotificationReadStateAsync(List<Guid> notificationUserIds, bool isRead, DateTime? updateDate)Marks multiple notifications as read or unread.
OrderNotificationController.cs
[ApiController]
[Route("api/orders/{orderId:guid}/notifications")]
public class OrderNotificationController : LgControllerBase
{
private readonly ILgNotificationManager<NotificationVm, Notification> _notificationManager;

public OrderNotificationController(ILgNotificationManager<NotificationVm, Notification> notificationManager)
{
_notificationManager = notificationManager;
}

[HttpPost("send")]
public async Task<IActionResult> SendAsync(Guid orderId, Guid userId)
{
try
{
Notification notification = new()
{
Title = "#OrderNotificationTitle",
Description = "#OrderNotificationDescription",
Category = "order"
};

// Persists the notification and triggers the SignalR refresh for the recipient.
await _notificationManager.SendNotificationAsync(notification, userId);
return Ok();
}
catch (Exception ex)
{
return Problem(ex);
}
}

[HttpDelete("user-notifications/{notificationUserId:guid}")]
public async Task<IActionResult> DeleteForUserAsync(Guid notificationUserId)
{
try
{
// Limits deletion to notifications belonging to the current user.
await _notificationManager.DeleteUserNotificationAsync(notificationUserId, ContextUserId);
return Ok();
}
catch (Exception ex)
{
return Problem(ex);
}
}
}

Internal endpoint contract​

The INotificationService<TItem> client calls the base controller on the route api/-/notification:

VerbRouteRole
GETapi/-/notification/GetNotificationsLoads notifications for the current user.
POSTapi/-/notification/GetNotificationsLoads notifications updated since the known item date.
GETapi/-/notification/UpdateNotificationReadState/{userNotificationId:guid}/{isRead}Changes the read / unread state of a notification.
POSTapi/-/notification/UpdateNotificationReadState/{isRead}Changes the read / unread state of a list of notifications.
POSTapi/-/notification/SendPendingActionsReplays actions performed offline.
DELETEapi/-/notification/DeleteUserNotification/{userNotificationId:guid}Deletes a user notification.
POSTapi/-/notification/DeleteUserNotificationDeletes a list of user notifications.

The SignalR hub used for notifications is hubs/notification and requires authentication.

Client-side — service & methods​

Inject INotificationService<NotificationVm> into a page or component. Call EnsureInitializedAsync before reading Notifications.

NotificationsPage.razor
<LgButton Text="#RefreshNotifications"
IconName="@IconNames.All.ArrowClockwise"
OnClick="RefreshNotificationsAsync" />
NotificationsPage.razor.cs
public partial class NotificationsPage : LgPage
{
[Inject]
private INotificationService<NotificationVm> NotificationService { get; set; }

private IReadOnlyCollection<NotificationVm> Notifications { get; set; }

protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();

// Initializes IndexedDB, starts the SignalR connection, and prepares the local collection.
await NotificationService.EnsureInitializedAsync();
NotificationService.OnNotificationChange += OnNotificationChange;
Notifications = NotificationService.Notifications;
}

private async Task RefreshNotificationsAsync(ActionEventArgs args)
{
try
{
// Forces a remote sync before re-reading the local collection.
await NotificationService.EnsureInitializedAsync(true, args.CancellationToken);
Notifications = NotificationService.Notifications;
}
catch (Exception ex)
{
ShowException(ex);
}
}

private void OnNotificationChange()
{
Notifications = NotificationService.Notifications;
StateHasChanged();
}
}

Client-side signatures​

Method / memberDescription
IReadOnlyCollection<TItem> NotificationsLocal collection loaded from IndexedDB.
event Action OnNotificationChangeEvent raised when the local list is updated.
Task EnsureInitializedAsync(bool fromRemote = false, CancellationToken cancellationToken = default)Initializes local storage and the hub; with fromRemote = true, also forces server synchronization.
Task RefreshNotifications()Re-reads the local collection and triggers OnNotificationChange.
Task SetReadStateItemAsync(TItem notification, bool isRead)Marks a notification as read / unread.
Task SetReadStateItemsAsync(IEnumerable<Guid> notificationUserIds, bool isRead)Marks multiple notifications as read / unread.
Task DeleteItemAsync(TItem notification)Deletes a notification from the ViewModel.
Task DeleteItemAsync(Guid notificationUserId)Deletes a notification by its user identifier.
Task DeleteItemsAsync(IEnumerable<Guid> notificationUserIds)Deletes multiple notifications.

In case of network loss, deletions and state changes are stored as pending actions in IndexedDB, then replayed to the server when the SignalR connection is restored.

Demonstration​

Try notifications in the Lagoon showcase.