From f8d3c9e61d4aa7bd8aba280e61b1d6c0950801c6 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 11 Aug 2025 11:00:05 +0200 Subject: [PATCH 1/2] Add page on SignalR server events --- 16/umbraco-cms/SUMMARY.md | 1 + 16/umbraco-cms/customizing/server-events.md | 118 ++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 16/umbraco-cms/customizing/server-events.md diff --git a/16/umbraco-cms/SUMMARY.md b/16/umbraco-cms/SUMMARY.md index ceaf121a857..d580c29e70e 100644 --- a/16/umbraco-cms/SUMMARY.md +++ b/16/umbraco-cms/SUMMARY.md @@ -221,6 +221,7 @@ * [Workspaces](customizing/workspaces.md) * [Umbraco Package](customizing/umbraco-package.md) * [UI Library](customizing/ui-library.md) +* [Server Events](customizing/server-events.md) * [Examples and Playground](examples-and-playground.md) ## Extending Umbraco diff --git a/16/umbraco-cms/customizing/server-events.md b/16/umbraco-cms/customizing/server-events.md new file mode 100644 index 00000000000..b2a95e0ef91 --- /dev/null +++ b/16/umbraco-cms/customizing/server-events.md @@ -0,0 +1,118 @@ +--- +description: Describes server events emitted via a SignalR hub and available for consumption in the backoffice +--- + +# Server Events + +Umbraco registers a SignalR event hub that broadcasts events related to the update of entities. These can be used in the backoffice to respond to changes made by users other than the current editor. + +Each server event is triggered via a notification handler. So for example, when a document is saved, the `ContentSavedNotification` is published. This is handled by a class responsible for issuing a server event. + +Not all server events should be broadcast to all users. For example, if a user doesn't have access to the Media section, they shouldn't receive notifications on updates to media. For core entities, Umbraco uses the same permission system that defines access in the backoffice. In this way, only events appropriate for the currently logged in editor are exposed. + +## Event Information + +Each event emitted contains the following fields: + +- `EventType` - the event type, which might be `Created`, `Updated`, `Deleted` etc. +- `EventSource` - the event source, which might be `Document`, `Media` etc. +- `Key` - the unique GUID that identifies the entity changed. + +## Event Authorization + +The currently authorized user will have one or more claims indicating the access they have to different areas of the backoffice. When first connecting to the SignalR hub, these details are used to assign the user to one or more SignalR groups. + +Which groups they have access to is determined by the collection of registered `IEventSourceAuthorizer` instances. + +For example, there is a `DocumentEventAuthorizer` that ensures users with access to the documents (content) tree are assigned to the `Umbraco:CMS:Document` event source. As a result, when server events are emitted for documents, only those users with this access will receive them. + +## Extending Server Events + +Using the same patterns as the core CMS, server events for other entities defined in packages or custom solutions can be emitted. + +Firstly, a `IEventSourceAuthorizer` should be registered. This will likely hook into what you already have in place for controlling access to the backoffice section where the entity is managed. + +For example, if you had a `Product` entity managed in a custom CMS section, the authorizer might look like this: + +```csharp +using Microsoft.AspNetCore.Authorization; +using Umbraco.Cms.Core; +using Umbraco.Cms.Web.Common.Authorization; + +public class ProductEventAuthorizer : EventSourcePolicyAuthorizer +{ + public ProductEventAuthorizer(IAuthorizationService authorizationService) + : base(authorizationService) + { + } + + public override IEnumerable AuthorizableEventSources => ["Umbraco:Custom:Product"]; + + protected override string Policy => "TreeAccessProducts"; // Maps to an existing authorization policy + // used in an [Authorize] attribute. +} +``` + +The authorizer should be registered with Umbraco using something like the following, called from a composer, or otherwise in application start up: + +```csharp +public static IUmbracoBuilder AddCustomAuthorizers(this IUmbracoBuilder builder) +{ + builder.EventSourceAuthorizers() + .Append(); +} +``` + +You then need to emit the event from a notification handler which handles notifications published when the entity is updated. + +For example, assuming an existing `ProductSavedNotification` that is published with the product is saved: + +```csharp +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Models.ServerEvents; +using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.ServerEvents; + +namespace Umbraco.Cms.Api.Management.ServerEvents; + +public class ProductServerEventSender : INotificationAsyncHandler +{ + private readonly IServerEventRouter _serverEventRouter; + + public ProductServerEventSender(IServerEventRouter serverEventRouter) => _serverEventRouter = serverEventRouter; + + public async Task HandleAsync(ProductSavedNotification notification, CancellationToken cancellationToken) => + await NotifySavedAsync(notification, "Umbraco:Custom:Product"); + + private async Task NotifySavedAsync(SavedNotification notification, string source) + where T : IEntity + { + foreach (T entity in notification.SavedEntities) + { + var eventModel = new ServerEvent + { + EventType = entity.CreateDate == entity.UpdateDate + ? Constants.ServerEvents.EventType.Created : Constants.ServerEvents.EventType.Updated, + Key = entity.Key, + EventSource = source, + }; + + await _serverEventRouter.RouteEventAsync(eventModel); + } + } +} +``` + +Again, the notification handler will need to be registered with Umbraco: + +```csharp +public static IUmbracoBuilder AddCustomEvents(this IUmbracoBuilder builder) +{ + builder.AddNotificationAsyncHandler(); +} +``` + + + From 2d0457ab98396001d4d93171c436d7efcea7d4a7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 11 Aug 2025 16:10:05 +0200 Subject: [PATCH 2/2] Moved article into 'Extending' section --- 16/umbraco-cms/SUMMARY.md | 2 +- 16/umbraco-cms/{customizing => extending}/server-events.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename 16/umbraco-cms/{customizing => extending}/server-events.md (100%) diff --git a/16/umbraco-cms/SUMMARY.md b/16/umbraco-cms/SUMMARY.md index d580c29e70e..aaef01134fa 100644 --- a/16/umbraco-cms/SUMMARY.md +++ b/16/umbraco-cms/SUMMARY.md @@ -221,7 +221,6 @@ * [Workspaces](customizing/workspaces.md) * [Umbraco Package](customizing/umbraco-package.md) * [UI Library](customizing/ui-library.md) -* [Server Events](customizing/server-events.md) * [Examples and Playground](examples-and-playground.md) ## Extending Umbraco @@ -250,6 +249,7 @@ * [Custom File Systems (IFileSystem)](extending/filesystemproviders/README.md) * [Using Azure Blob Storage for Media and ImageSharp Cache](extending/filesystemproviders/azure-blob-storage.md) * [Configuring Azure Key Vault](extending/key-vault.md) +* [Server Events From SignalR](extending/server-events.md) * [Packages](extending/packages/README.md) * [Creating a Package](extending/packages/creating-a-package.md) * [Language file for packages](extending/packages/language-files-for-packages.md) diff --git a/16/umbraco-cms/customizing/server-events.md b/16/umbraco-cms/extending/server-events.md similarity index 100% rename from 16/umbraco-cms/customizing/server-events.md rename to 16/umbraco-cms/extending/server-events.md