diff --git a/src/Umbraco.Core/Composing/AsyncComponentBase.cs b/src/Umbraco.Core/Composing/AsyncComponentBase.cs new file mode 100644 index 000000000000..5ba3dd168df4 --- /dev/null +++ b/src/Umbraco.Core/Composing/AsyncComponentBase.cs @@ -0,0 +1,55 @@ +namespace Umbraco.Cms.Core.Composing; + +/// +/// +/// By default, the component will not execute if Umbraco is restarting. +/// +public abstract class AsyncComponentBase : IAsyncComponent +{ + /// + public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken) + { + if (CanExecute(isRestarting)) + { + await InitializeAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken) + { + if (CanExecute(isRestarting)) + { + await TerminateAsync(cancellationToken).ConfigureAwait(false); + } + } + + /// + /// Determines whether the component can execute. + /// + /// If set to true indicates Umbraco is restarting. + /// + /// true if the component can execute; otherwise, false. + /// + protected virtual bool CanExecute(bool isRestarting) + => isRestarting is false; + + /// + /// Initializes the component. + /// + /// The cancellation token. Cancellation indicates that the start process has been aborted. + /// + /// A representing the asynchronous operation. + /// + protected abstract Task InitializeAsync(CancellationToken cancellationToken); + + /// + /// Terminates the component. + /// + /// The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful. + /// + /// A representing the asynchronous operation. + /// + protected virtual Task TerminateAsync(CancellationToken cancellationToken) + => Task.CompletedTask; +} diff --git a/src/Umbraco.Core/Composing/ComponentCollection.cs b/src/Umbraco.Core/Composing/ComponentCollection.cs index 506eb2313483..ba49a6f48227 100644 --- a/src/Umbraco.Core/Composing/ComponentCollection.cs +++ b/src/Umbraco.Core/Composing/ComponentCollection.cs @@ -5,59 +5,60 @@ namespace Umbraco.Cms.Core.Composing; /// -/// Represents the collection of implementations. +/// Represents the collection of implementations. /// -public class ComponentCollection : BuilderCollectionBase +public class ComponentCollection : BuilderCollectionBase { private const int LogThresholdMilliseconds = 100; - private readonly ILogger _logger; private readonly IProfilingLogger _profilingLogger; + private readonly ILogger _logger; - public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) + public ComponentCollection(Func> items, IProfilingLogger profilingLogger, ILogger logger) : base(items) { _profilingLogger = profilingLogger; _logger = logger; } - public void Initialize() + public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken) { - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) + using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false + ? null + : _profilingLogger.DebugDuration($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized.")) { - foreach (IComponent component in this) + foreach (IAsyncComponent component in this) { Type componentType = component.GetType(); - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Initializing {componentType.FullName}.", - $"Initialized {componentType.FullName}.", - thresholdMilliseconds: LogThresholdMilliseconds)) + + using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false + ? null : + _profilingLogger.DebugDuration($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { - component.Initialize(); + await component.InitializeAsync(isRestarting, cancellationToken); } } } } - public void Terminate() + public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken) { - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) + using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) + ? null + : _profilingLogger.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated.")) { // terminate components in reverse order - foreach (IComponent component in this.Reverse()) + foreach (IAsyncComponent component in this.Reverse()) { Type componentType = component.GetType(); - using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration( - $"Terminating {componentType.FullName}.", - $"Terminated {componentType.FullName}.", - thresholdMilliseconds: LogThresholdMilliseconds)) + + using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false + ? null + : _profilingLogger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { try { - component.Terminate(); - component.DisposeIfDisposable(); + await component.TerminateAsync(isRestarting, cancellationToken); } catch (Exception ex) { diff --git a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs index 04461db4fbd0..f4cdb802387e 100644 --- a/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs +++ b/src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs @@ -4,35 +4,33 @@ namespace Umbraco.Cms.Core.Composing; /// -/// Builds a . +/// Builds a . /// -public class - ComponentCollectionBuilder : OrderedCollectionBuilderBase +public class ComponentCollectionBuilder : OrderedCollectionBuilderBase { private const int LogThresholdMilliseconds = 100; protected override ComponentCollectionBuilder This => this; - protected override IEnumerable CreateItems(IServiceProvider factory) + protected override IEnumerable CreateItems(IServiceProvider factory) { IProfilingLogger logger = factory.GetRequiredService(); - using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration( - $"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) + using (logger.IsEnabled(Logging.LogLevel.Debug) is false + ? null + : logger.DebugDuration($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created.")) { return base.CreateItems(factory); } } - protected override IComponent CreateItem(IServiceProvider factory, Type itemType) + protected override IAsyncComponent CreateItem(IServiceProvider factory, Type itemType) { IProfilingLogger logger = factory.GetRequiredService(); - using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration( - $"Creating {itemType.FullName}.", - $"Created {itemType.FullName}.", - thresholdMilliseconds: LogThresholdMilliseconds)) + using (logger.IsEnabled(Logging.LogLevel.Debug) is false + ? null : + logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds)) { return base.CreateItem(factory, itemType); } diff --git a/src/Umbraco.Core/Composing/ComponentComposer.cs b/src/Umbraco.Core/Composing/ComponentComposer.cs index 2a9641e64b5a..6beee43766cb 100644 --- a/src/Umbraco.Core/Composing/ComponentComposer.cs +++ b/src/Umbraco.Core/Composing/ComponentComposer.cs @@ -1,18 +1,23 @@ -using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Core.Composing; /// -/// Provides a base class for composers which compose a component. +/// Provides a composer that appends a component. /// -/// The type of the component +/// The type of the component. +/// +/// Thanks to this class, a component that does not compose anything can be registered with one line: +/// +/// { } +/// ]]> +/// +/// public abstract class ComponentComposer : IComposer - where TComponent : IComponent + where TComponent : IAsyncComponent { /// - public virtual void Compose(IUmbracoBuilder builder) => builder.Components().Append(); - - // note: thanks to this class, a component that does not compose anything can be - // registered with one line: - // public class MyComponentComposer : ComponentComposer { } + public virtual void Compose(IUmbracoBuilder builder) + => builder.Components().Append(); } diff --git a/src/Umbraco.Core/Composing/IAsyncComponent.cs b/src/Umbraco.Core/Composing/IAsyncComponent.cs new file mode 100644 index 000000000000..b84e95887990 --- /dev/null +++ b/src/Umbraco.Core/Composing/IAsyncComponent.cs @@ -0,0 +1,35 @@ +namespace Umbraco.Cms.Core.Composing; + +/// +/// Represents a component. +/// +/// +/// +/// Components are created by DI and therefore must have a public constructor. +/// +/// +/// All components are terminated in reverse order when Umbraco terminates, and disposable components are disposed. +/// +/// +public interface IAsyncComponent +{ + /// + /// Initializes the component. + /// + /// If set to true indicates Umbraco is restarting. + /// The cancellation token. Cancellation indicates that the start process has been aborted. + /// + /// A representing the asynchronous operation. + /// + Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken); + + /// + /// Terminates the component. + /// + /// If set to true indicates Umbraco is restarting. + /// The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful. + /// + /// A representing the asynchronous operation. + /// + Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken); +} diff --git a/src/Umbraco.Core/Composing/IComponent.cs b/src/Umbraco.Core/Composing/IComponent.cs index d5655f8a1f81..2c3b9b7e4f81 100644 --- a/src/Umbraco.Core/Composing/IComponent.cs +++ b/src/Umbraco.Core/Composing/IComponent.cs @@ -1,28 +1,32 @@ namespace Umbraco.Cms.Core.Composing; -/// -/// Represents a component. -/// -/// -/// Components are created by DI and therefore must have a public constructor. -/// -/// All components are terminated in reverse order when Umbraco terminates, and -/// disposable components are disposed. -/// -/// -/// The Dispose method may be invoked more than once, and components -/// should ensure they support this. -/// -/// -public interface IComponent +/// +[Obsolete("Use IAsyncComponent instead. This interface will be removed in a future version.")] +public interface IComponent : IAsyncComponent { /// - /// Initializes the component. + /// Initializes the component. /// void Initialize(); /// - /// Terminates the component. + /// Terminates the component. /// void Terminate(); + + /// + Task IAsyncComponent.InitializeAsync(bool isRestarting, CancellationToken cancellationToken) + { + Initialize(); + + return Task.CompletedTask; + } + + /// + Task IAsyncComponent.TerminateAsync(bool isRestarting, CancellationToken cancellationToken) + { + Terminate(); + + return Task.CompletedTask; + } } diff --git a/src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs b/src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs new file mode 100644 index 000000000000..d4e326e05810 --- /dev/null +++ b/src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs @@ -0,0 +1,23 @@ +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Core.Composing; + +/// +/// +/// By default, the component will not execute if Umbraco is restarting or the runtime level is not . +/// +public abstract class RuntimeAsyncComponentBase : AsyncComponentBase +{ + private readonly IRuntimeState _runtimeState; + + /// + /// Initializes a new instance of the class. + /// + /// State of the Umbraco runtime. + protected RuntimeAsyncComponentBase(IRuntimeState runtimeState) + => _runtimeState = runtimeState; + + /// + protected override bool CanExecute(bool isRestarting) + => base.CanExecute(isRestarting) && _runtimeState.Level == RuntimeLevel.Run; +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs index af42e0993630..e1536f9a4d8a 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.CollectionBuilders.cs @@ -21,7 +21,7 @@ public static partial class UmbracoBuilderExtensions /// The builder. /// public static IUmbracoBuilder AddComponent(this IUmbracoBuilder builder) - where T : IComponent + where T : IAsyncComponent { builder.Components().Append(); diff --git a/src/Umbraco.Core/Extensions/ObjectExtensions.cs b/src/Umbraco.Core/Extensions/ObjectExtensions.cs index f0f10d8cc653..1ce15bdc72c5 100644 --- a/src/Umbraco.Core/Extensions/ObjectExtensions.cs +++ b/src/Umbraco.Core/Extensions/ObjectExtensions.cs @@ -41,8 +41,9 @@ public static class ObjectExtensions public static IEnumerable AsEnumerableOfOne(this T input) => Enumerable.Repeat(input, 1); /// + /// Disposes the object if it implements . /// - /// + /// The object. public static void DisposeIfDisposable(this object input) { if (input is IDisposable disposable) diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs index 9172359eb0aa..63c733f3cb77 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStartingNotification.cs @@ -1,28 +1,26 @@ namespace Umbraco.Cms.Core.Notifications; /// -/// Notification that occurs at the very end of the Umbraco boot process (after all s are -/// initialized). +/// Notification that occurs at the very end of the Umbraco boot process (after all components are initialized). +/// +public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification +{ + /// + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification + /// The runtime level + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) { - /// - /// Initializes a new instance of the class. - /// - /// The runtime level - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting) - { - RuntimeLevel = runtimeLevel; - IsRestarting = isRestarting; - } + RuntimeLevel = runtimeLevel; + IsRestarting = isRestarting; + } /// - /// Gets the runtime level. + /// Gets the runtime level. /// /// - /// The runtime level. + /// The runtime level. /// public RuntimeLevel RuntimeLevel { get; } diff --git a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs index d33233d43851..43058fe27f09 100644 --- a/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs +++ b/src/Umbraco.Core/Notifications/UmbracoApplicationStoppingNotification.cs @@ -1,17 +1,16 @@ namespace Umbraco.Cms.Core.Notifications; - +/// +/// Notification that occurs when Umbraco is shutting down (after all components are terminated). +/// +public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification +{ /// - /// Notification that occurs when Umbraco is shutting down (after all s are terminated). + /// Initializes a new instance of the class. /// - /// - public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification - { - /// - /// Initializes a new instance of the class. - /// - /// Indicates whether Umbraco is restarting. - public UmbracoApplicationStoppingNotification(bool isRestarting) => IsRestarting = isRestarting; + /// Indicates whether Umbraco is restarting. + public UmbracoApplicationStoppingNotification(bool isRestarting) + => IsRestarting = isRestarting; /// public bool IsRestarting { get; } diff --git a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs index 42f7f6de3ffe..2e4604904238 100644 --- a/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs +++ b/src/Umbraco.Infrastructure/Runtime/CoreRuntime.cs @@ -222,7 +222,7 @@ private async Task StartAsync(CancellationToken cancellationToken, bool isRestar } // Initialize the components - _components.Initialize(); + await _components.InitializeAsync(isRestarting, cancellationToken); await _eventAggregator.PublishAsync(new UmbracoApplicationStartingNotification(State.Level, isRestarting), cancellationToken); @@ -236,7 +236,7 @@ private async Task StartAsync(CancellationToken cancellationToken, bool isRestar private async Task StopAsync(CancellationToken cancellationToken, bool isRestarting) { - _components.Terminate(); + await _components.TerminateAsync(isRestarting, cancellationToken); await _eventAggregator.PublishAsync(new UmbracoApplicationStoppingNotification(isRestarting), cancellationToken); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs index ca47bfbd9707..94bff4a0c904 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Components/ComponentTests.cs @@ -80,7 +80,9 @@ private static IServiceProvider MockFactory(Action> setup Options.Create(new ContentSettings())); var eventAggregator = Mock.Of(); var scopeProvider = new ScopeProvider( - new AmbientScopeStack(), new AmbientScopeContextStack(),Mock.Of(), + new AmbientScopeStack(), + new AmbientScopeContextStack(), + Mock.Of(), f, fs, new TestOptionsMonitor(coreDebug), @@ -113,7 +115,7 @@ private static IServiceProvider MockFactory(Action> setup Mock.Of()); [Test] - public void Boot1A() + public async Task Boot1A() { var register = MockRegister(); var composition = new UmbracoBuilder(register, Mock.Of(), TestHelper.GetMockedTypeLoader()); @@ -157,7 +159,7 @@ public void Boot1A() { return Mock.Of>(); } - + if (type == typeof(ILogger)) { return Mock.Of>(); @@ -176,9 +178,9 @@ public void Boot1A() var components = builder.CreateCollection(factory); Assert.IsEmpty(components); - components.Initialize(); + await components.InitializeAsync(false, default); Assert.IsEmpty(Initialized); - components.Terminate(); + await components.TerminateAsync(false, default); Assert.IsEmpty(Terminated); } @@ -277,7 +279,7 @@ public void BrokenRequired() } [Test] - public void Initialize() + public async Task Initialize() { Composed.Clear(); Initialized.Clear(); @@ -324,7 +326,7 @@ public void Initialize() { return Mock.Of>(); } - + if (type == typeof(IServiceProviderIsService)) { return Mock.Of(); @@ -347,11 +349,11 @@ public void Initialize() var components = builder.CreateCollection(factory); Assert.IsEmpty(Initialized); - components.Initialize(); + await components.InitializeAsync(false, default); AssertTypeArray(TypeArray(), Initialized); Assert.IsEmpty(Terminated); - components.Terminate(); + await components.TerminateAsync(false, default); AssertTypeArray(TypeArray(), Terminated); }