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 2ef1f9194f76..21fa8fa3b4c4 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);
}