Added support for sync notification handlers in the event aggregator

This commit is contained in:
Bjarke Berg
2021-01-19 09:57:55 +01:00
parent 9d0665e66c
commit 69724d8d2b
11 changed files with 146 additions and 27 deletions

View File

@@ -2,7 +2,6 @@
// See LICENSE for more details.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Umbraco.Core.Events;
namespace Umbraco.Core.DependencyInjection
@@ -35,5 +34,29 @@ namespace Umbraco.Core.DependencyInjection
return builder;
}
/// <summary>
/// Registers a notification async handler against the Umbraco service collection.
/// </summary>
/// <typeparam name="TNotification">The type of notification.</typeparam>
/// <typeparam name="TNotificationAsyncHandler">The type of notification async handler.</typeparam>
/// <param name="builder">The Umbraco builder.</param>
/// <returns>The <see cref="IUmbracoBuilder"/>.</returns>
public static IUmbracoBuilder AddNotificationAsyncHandler<TNotification, TNotificationAsyncHandler>(this IUmbracoBuilder builder)
where TNotificationAsyncHandler : INotificationAsyncHandler<TNotification>
where TNotification : INotification
{
// Register the handler as transient. This ensures that anything can be injected into it.
var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler<TNotification>), typeof(TNotificationAsyncHandler), ServiceLifetime.Transient);
// TODO: Waiting on feedback here https://github.com/umbraco/Umbraco-CMS/pull/9556/files#r548365396 about whether
// we perform this duplicate check or not.
if (!builder.Services.Contains(descriptor))
{
builder.Services.Add(descriptor);
}
return builder;
}
}
}

View File

@@ -146,7 +146,7 @@ namespace Umbraco.Core.DependencyInjection
Services.AddSingleton<ManifestWatcher>();
Services.AddSingleton<UmbracoRequestPaths>();
this.AddNotificationHandler<UmbracoApplicationStarting, AppPluginsManifestWatcherNotificationHandler>();
this.AddNotificationAsyncHandler<UmbracoApplicationStarting, AppPluginsManifestWatcherNotificationHandler>();
Services.AddUnique<InstallStatusTracker>();

View File

@@ -15,17 +15,30 @@ namespace Umbraco.Core.Events
/// </content>
public partial class EventAggregator : IEventAggregator
{
private static readonly ConcurrentDictionary<Type, NotificationAsyncHandlerWrapper> s_notificationAsyncHandlers
= new ConcurrentDictionary<Type, NotificationAsyncHandlerWrapper>();
private static readonly ConcurrentDictionary<Type, NotificationHandlerWrapper> s_notificationHandlers
= new ConcurrentDictionary<Type, NotificationHandlerWrapper>();
private Task PublishNotificationAsync(INotification notification, CancellationToken cancellationToken = default)
{
Type notificationType = notification.GetType();
NotificationHandlerWrapper handler = s_notificationHandlers.GetOrAdd(
NotificationAsyncHandlerWrapper asyncHandler = s_notificationAsyncHandlers.GetOrAdd(
notificationType,
t => (NotificationAsyncHandlerWrapper)Activator.CreateInstance(typeof(NotificationAsyncHandlerWrapperImpl<>).MakeGenericType(notificationType)));
return asyncHandler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync);
}
private void PublishNotification(INotification notification)
{
Type notificationType = notification.GetType();
NotificationHandlerWrapper asyncHandler = s_notificationHandlers.GetOrAdd(
notificationType,
t => (NotificationHandlerWrapper)Activator.CreateInstance(typeof(NotificationHandlerWrapperImpl<>).MakeGenericType(notificationType)));
return handler.HandleAsync(notification, cancellationToken, _serviceFactory, PublishCoreAsync);
asyncHandler.Handle(notification, _serviceFactory, PublishCore);
}
private async Task PublishCoreAsync(
@@ -38,9 +51,27 @@ namespace Umbraco.Core.Events
await handler(notification, cancellationToken).ConfigureAwait(false);
}
}
private void PublishCore(
IEnumerable<Action<INotification>> allHandlers,
INotification notification)
{
foreach (Action<INotification> handler in allHandlers)
{
handler(notification);
}
}
}
internal abstract class NotificationHandlerWrapper
{
public abstract void Handle(
INotification notification,
ServiceFactory serviceFactory,
Action<IEnumerable<Action<INotification>>, INotification> publish);
}
internal abstract class NotificationAsyncHandlerWrapper
{
public abstract Task HandleAsync(
INotification notification,
@@ -49,7 +80,7 @@ namespace Umbraco.Core.Events
Func<IEnumerable<Func<INotification, CancellationToken, Task>>, INotification, CancellationToken, Task> publish);
}
internal class NotificationHandlerWrapperImpl<TNotification> : NotificationHandlerWrapper
internal class NotificationAsyncHandlerWrapperImpl<TNotification> : NotificationAsyncHandlerWrapper
where TNotification : INotification
{
public override Task HandleAsync(
@@ -59,7 +90,7 @@ namespace Umbraco.Core.Events
Func<IEnumerable<Func<INotification, CancellationToken, Task>>, INotification, CancellationToken, Task> publish)
{
IEnumerable<Func<INotification, CancellationToken, Task>> handlers = serviceFactory
.GetInstances<INotificationHandler<TNotification>>()
.GetInstances<INotificationAsyncHandler<TNotification>>()
.Select(x => new Func<INotification, CancellationToken, Task>(
(theNotification, theToken) =>
x.HandleAsync((TNotification)theNotification, theToken)));
@@ -67,4 +98,22 @@ namespace Umbraco.Core.Events
return publish(handlers, notification, cancellationToken);
}
}
internal class NotificationHandlerWrapperImpl<TNotification> : NotificationHandlerWrapper
where TNotification : INotification
{
public override void Handle(
INotification notification,
ServiceFactory serviceFactory,
Action<IEnumerable<Action<INotification>>, INotification> publish)
{
IEnumerable<Action<INotification>> handlers = serviceFactory
.GetInstances<INotificationHandler<TNotification>>()
.Select(x => new Action<INotification>(
(theNotification) =>
x.Handle((TNotification)theNotification)));
publish(handlers, notification);
}
}
}

View File

@@ -38,6 +38,7 @@ namespace Umbraco.Core.Events
throw new ArgumentNullException(nameof(notification));
}
PublishNotification(notification);
return PublishNotificationAsync(notification, cancellationToken);
}
}

View File

@@ -13,7 +13,7 @@ namespace Umbraco.Core.Events
public interface IEventAggregator
{
/// <summary>
/// Asynchronously send a notification to multiple handlers
/// Asynchronously send a notification to multiple handlers of both sync and async
/// </summary>
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
/// <param name="notification">The notification object.</param>

View File

@@ -12,6 +12,20 @@ namespace Umbraco.Core.Events
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
public interface INotificationHandler<in TNotification>
where TNotification : INotification
{
/// <summary>
/// Handles a notification
/// </summary>
/// <param name="notification">The notification</param>
void Handle(TNotification notification);
}
/// <summary>
/// Defines a handler for a async notification.
/// </summary>
/// <typeparam name="TNotification">The type of notification being handled.</typeparam>
public interface INotificationAsyncHandler<in TNotification>
where TNotification : INotification
{
/// <summary>
/// Handles a notification

View File

@@ -11,7 +11,7 @@ namespace Umbraco.Core.Runtime
/// <summary>
/// Starts monitoring AppPlugins directory during debug runs, to restart site when a plugin manifest changes.
/// </summary>
public sealed class AppPluginsManifestWatcherNotificationHandler : INotificationHandler<UmbracoApplicationStarting>
public sealed class AppPluginsManifestWatcherNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStarting>
{
private readonly ManifestWatcher _manifestWatcher;
private readonly IHostingEnvironment _hostingEnvironment;

View File

@@ -1,5 +1,3 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Options;
using Umbraco.Core.Configuration.Models;
using Umbraco.Core.Events;
@@ -21,7 +19,7 @@ namespace Umbraco.Core.Runtime
_globalSettings = globalSettings.Value;
}
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
// ensure we have some essential directories
// every other component can then initialize safely
@@ -31,7 +29,6 @@ namespace Umbraco.Core.Runtime
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.PartialViews));
_ioHelper.EnsurePathExists(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.MacroPartials));
return Task.CompletedTask;
}
}
}

View File

@@ -1,5 +1,3 @@
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Events;
using Umbraco.Core.Persistence;
@@ -39,7 +37,7 @@ namespace Umbraco.Infrastructure.Cache
}
/// <inheritdoc/>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
// The scheduled tasks - TouchServerTask and InstructionProcessTask - run as .NET Core hosted services.
// The former (as well as other hosted services that run outside of an HTTP request context) depends on the application URL
@@ -50,8 +48,6 @@ namespace Umbraco.Infrastructure.Cache
_requestAccessor.EndRequest += EndRequest;
Startup();
return Task.CompletedTask;
}
private void Startup()

View File

@@ -28,6 +28,22 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events
_builder = new UmbracoBuilder(register, Mock.Of<IConfiguration>(), TestHelper.GetMockedTypeLoader());
}
[Test]
public async Task CanPublishAsyncEvents()
{
_builder.Services.AddScoped<Adder>();
_builder.AddNotificationAsyncHandler<Notification, NotificationAsyncHandlerA>();
_builder.AddNotificationAsyncHandler<Notification, NotificationAsyncHandlerB>();
_builder.AddNotificationAsyncHandler<Notification, NotificationAsyncHandlerC>();
ServiceProvider provider = _builder.Services.BuildServiceProvider();
var notification = new Notification();
IEventAggregator aggregator = provider.GetService<IEventAggregator>();
await aggregator.PublishAsync(notification);
Assert.AreEqual(A + B + C, notification.SubscriberCount);
}
[Test]
public async Task CanPublishEvents()
{
@@ -51,19 +67,17 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events
public class NotificationHandlerA : INotificationHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
public void Handle(Notification notification)
{
notification.SubscriberCount += A;
return Task.CompletedTask;
}
}
public class NotificationHandlerB : INotificationHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
public void Handle(Notification notification)
{
notification.SubscriberCount += B;
return Task.CompletedTask;
}
}
@@ -73,13 +87,42 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events
public NotificationHandlerC(Adder adder) => _adder = adder;
public void Handle(Notification notification)
{
notification.SubscriberCount = _adder.Add(notification.SubscriberCount, C);
}
}
public class NotificationAsyncHandlerA : INotificationAsyncHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
{
notification.SubscriberCount += A;
return Task.CompletedTask;
}
}
public class NotificationAsyncHandlerB : INotificationAsyncHandler<Notification>
{
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
{
notification.SubscriberCount += B;
return Task.CompletedTask;
}
}
public class NotificationAsyncHandlerC : INotificationAsyncHandler<Notification>
{
private readonly Adder _adder;
public NotificationAsyncHandlerC(Adder adder) => _adder = adder;
public Task HandleAsync(Notification notification, CancellationToken cancellationToken)
{
notification.SubscriberCount = _adder.Add(notification.SubscriberCount, C);
return Task.CompletedTask;
}
}
public class Adder
{
public int Add(int a, int b) => a + b;

View File

@@ -1,8 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Umbraco.Core.Events;
using Umbraco.Core.Logging;
@@ -46,7 +44,7 @@ namespace Umbraco.Web.Common.Profiler
}
/// <inheritdoc/>
public Task HandleAsync(UmbracoApplicationStarting notification, CancellationToken cancellationToken)
public void Handle(UmbracoApplicationStarting notification)
{
if (_profile)
{
@@ -57,8 +55,6 @@ namespace Umbraco.Web.Common.Profiler
// Stop the profiling of the booting process
_profiler.StopBoot();
}
return Task.CompletedTask;
}
}
}