diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs index c24936b4fb..2b4f05a4f0 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Events.cs @@ -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; } + + /// + /// Registers a notification async handler against the Umbraco service collection. + /// + /// The type of notification. + /// The type of notification async handler. + /// The Umbraco builder. + /// The . + public static IUmbracoBuilder AddNotificationAsyncHandler(this IUmbracoBuilder builder) + where TNotificationAsyncHandler : INotificationAsyncHandler + where TNotification : INotification + { + // Register the handler as transient. This ensures that anything can be injected into it. + var descriptor = new ServiceDescriptor(typeof(INotificationAsyncHandler), 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; + } } } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 54e5982a58..b69a2c1677 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -146,7 +146,7 @@ namespace Umbraco.Core.DependencyInjection Services.AddSingleton(); Services.AddSingleton(); - this.AddNotificationHandler(); + this.AddNotificationAsyncHandler(); Services.AddUnique(); diff --git a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs index 00982b52d7..faadc2fc59 100644 --- a/src/Umbraco.Core/Events/EventAggregator.Notifications.cs +++ b/src/Umbraco.Core/Events/EventAggregator.Notifications.cs @@ -15,17 +15,30 @@ namespace Umbraco.Core.Events /// public partial class EventAggregator : IEventAggregator { + private static readonly ConcurrentDictionary s_notificationAsyncHandlers + = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_notificationHandlers = new ConcurrentDictionary(); 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> allHandlers, + INotification notification) + { + foreach (Action handler in allHandlers) + { + handler(notification); + } + } } internal abstract class NotificationHandlerWrapper + { + public abstract void Handle( + INotification notification, + ServiceFactory serviceFactory, + Action>, INotification> publish); + } + + internal abstract class NotificationAsyncHandlerWrapper { public abstract Task HandleAsync( INotification notification, @@ -49,7 +80,7 @@ namespace Umbraco.Core.Events Func>, INotification, CancellationToken, Task> publish); } - internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper + internal class NotificationAsyncHandlerWrapperImpl : NotificationAsyncHandlerWrapper where TNotification : INotification { public override Task HandleAsync( @@ -59,7 +90,7 @@ namespace Umbraco.Core.Events Func>, INotification, CancellationToken, Task> publish) { IEnumerable> handlers = serviceFactory - .GetInstances>() + .GetInstances>() .Select(x => new Func( (theNotification, theToken) => x.HandleAsync((TNotification)theNotification, theToken))); @@ -67,4 +98,22 @@ namespace Umbraco.Core.Events return publish(handlers, notification, cancellationToken); } } + + internal class NotificationHandlerWrapperImpl : NotificationHandlerWrapper + where TNotification : INotification + { + public override void Handle( + INotification notification, + ServiceFactory serviceFactory, + Action>, INotification> publish) + { + IEnumerable> handlers = serviceFactory + .GetInstances>() + .Select(x => new Action( + (theNotification) => + x.Handle((TNotification)theNotification))); + + publish(handlers, notification); + } + } } diff --git a/src/Umbraco.Core/Events/EventAggregator.cs b/src/Umbraco.Core/Events/EventAggregator.cs index 8d58175da4..7bc63df228 100644 --- a/src/Umbraco.Core/Events/EventAggregator.cs +++ b/src/Umbraco.Core/Events/EventAggregator.cs @@ -38,6 +38,7 @@ namespace Umbraco.Core.Events throw new ArgumentNullException(nameof(notification)); } + PublishNotification(notification); return PublishNotificationAsync(notification, cancellationToken); } } diff --git a/src/Umbraco.Core/Events/IEventAggregator.cs b/src/Umbraco.Core/Events/IEventAggregator.cs index d78aed36b7..dc4205b0ec 100644 --- a/src/Umbraco.Core/Events/IEventAggregator.cs +++ b/src/Umbraco.Core/Events/IEventAggregator.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Events public interface IEventAggregator { /// - /// Asynchronously send a notification to multiple handlers + /// Asynchronously send a notification to multiple handlers of both sync and async /// /// The type of notification being handled. /// The notification object. diff --git a/src/Umbraco.Core/Events/INotificationHandler.cs b/src/Umbraco.Core/Events/INotificationHandler.cs index dc5d83e0f7..4cfe52e005 100644 --- a/src/Umbraco.Core/Events/INotificationHandler.cs +++ b/src/Umbraco.Core/Events/INotificationHandler.cs @@ -12,6 +12,20 @@ namespace Umbraco.Core.Events /// The type of notification being handled. public interface INotificationHandler where TNotification : INotification + { + /// + /// Handles a notification + /// + /// The notification + void Handle(TNotification notification); + } + + /// + /// Defines a handler for a async notification. + /// + /// The type of notification being handled. + public interface INotificationAsyncHandler + where TNotification : INotification { /// /// Handles a notification diff --git a/src/Umbraco.Core/Runtime/AppPluginsManifestWatcherNotificationHandler.cs b/src/Umbraco.Core/Runtime/AppPluginsManifestWatcherNotificationHandler.cs index 5f219ac51f..2b1fa3d0cc 100644 --- a/src/Umbraco.Core/Runtime/AppPluginsManifestWatcherNotificationHandler.cs +++ b/src/Umbraco.Core/Runtime/AppPluginsManifestWatcherNotificationHandler.cs @@ -11,7 +11,7 @@ namespace Umbraco.Core.Runtime /// /// Starts monitoring AppPlugins directory during debug runs, to restart site when a plugin manifest changes. /// - public sealed class AppPluginsManifestWatcherNotificationHandler : INotificationHandler + public sealed class AppPluginsManifestWatcherNotificationHandler : INotificationAsyncHandler { private readonly ManifestWatcher _manifestWatcher; private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs index 92ad5808b3..535a466e25 100644 --- a/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs +++ b/src/Umbraco.Core/Runtime/EssentialDirectoryCreator.cs @@ -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; } } } diff --git a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs index 5b4dd4f0ed..1ad2b44740 100644 --- a/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/Cache/DatabaseServerMessengerNotificationHandler.cs @@ -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 } /// - 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() diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs index cbf3d02542..57a3fb0bd7 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Events/EventAggregatorTests.cs @@ -28,6 +28,22 @@ namespace Umbraco.Tests.UnitTests.Umbraco.Core.Events _builder = new UmbracoBuilder(register, Mock.Of(), TestHelper.GetMockedTypeLoader()); } + [Test] + public async Task CanPublishAsyncEvents() + { + _builder.Services.AddScoped(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationAsyncHandler(); + _builder.AddNotificationAsyncHandler(); + ServiceProvider provider = _builder.Services.BuildServiceProvider(); + + var notification = new Notification(); + IEventAggregator aggregator = provider.GetService(); + 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 { - public Task HandleAsync(Notification notification, CancellationToken cancellationToken) + public void Handle(Notification notification) { notification.SubscriberCount += A; - return Task.CompletedTask; } } public class NotificationHandlerB : INotificationHandler { - 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 + { + public Task HandleAsync(Notification notification, CancellationToken cancellationToken) + { + notification.SubscriberCount += A; + return Task.CompletedTask; + } + } + + public class NotificationAsyncHandlerB : INotificationAsyncHandler + { + public Task HandleAsync(Notification notification, CancellationToken cancellationToken) + { + notification.SubscriberCount += B; + return Task.CompletedTask; + } + } + + public class NotificationAsyncHandlerC : INotificationAsyncHandler + { + 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; diff --git a/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs b/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs index fede88e14f..e939355c42 100644 --- a/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs +++ b/src/Umbraco.Web.Common/Profiler/InitializeWebProfiling.cs @@ -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 } /// - 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; } } }