Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IAsyncComponent to allow async initialize/terminate #16536

Merged
merged 4 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions src/Umbraco.Core/Composing/AsyncComponentBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
namespace Umbraco.Cms.Core.Composing;

/// <inheritdoc />
/// <remarks>
/// By default, the component will not execute if Umbraco is restarting.
/// </remarks>
public abstract class AsyncComponentBase : IAsyncComponent
{
/// <inheritdoc />
public async Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken)
{
if (CanExecute(isRestarting))
{
await InitializeAsync(cancellationToken).ConfigureAwait(false);
}
}

/// <inheritdoc />
public async Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken)
{
if (CanExecute(isRestarting))
{
await TerminateAsync(cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Determines whether the component can execute.
/// </summary>
/// <param name="isRestarting">If set to <c>true</c> indicates Umbraco is restarting.</param>
/// <returns>
/// <c>true</c> if the component can execute; otherwise, <c>false</c>.
/// </returns>
protected virtual bool CanExecute(bool isRestarting)
=> isRestarting is false;

/// <summary>
/// Initializes the component.
/// </summary>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the start process has been aborted.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
protected abstract Task InitializeAsync(CancellationToken cancellationToken);

/// <summary>
/// Terminates the component.
/// </summary>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
protected virtual Task TerminateAsync(CancellationToken cancellationToken)
=> Task.CompletedTask;
}
47 changes: 24 additions & 23 deletions src/Umbraco.Core/Composing/ComponentCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,60 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents the collection of <see cref="IComponent" /> implementations.
/// Represents the collection of <see cref="IAsyncComponent" /> implementations.
/// </summary>
public class ComponentCollection : BuilderCollectionBase<IComponent>
public class ComponentCollection : BuilderCollectionBase<IAsyncComponent>
{
private const int LogThresholdMilliseconds = 100;
private readonly ILogger<ComponentCollection> _logger;

private readonly IProfilingLogger _profilingLogger;
private readonly ILogger<ComponentCollection> _logger;

public ComponentCollection(Func<IEnumerable<IComponent>> items, IProfilingLogger profilingLogger, ILogger<ComponentCollection> logger)
public ComponentCollection(Func<IEnumerable<IAsyncComponent>> items, IProfilingLogger profilingLogger, ILogger<ComponentCollection> 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<ComponentCollection>(
$"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized."))
using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false
? null
: _profilingLogger.DebugDuration<ComponentCollection>($"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<ComponentCollection>(
$"Initializing {componentType.FullName}.",
$"Initialized {componentType.FullName}.",
thresholdMilliseconds: LogThresholdMilliseconds))

using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false
? null :
_profilingLogger.DebugDuration<ComponentCollection>($"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<ComponentCollection>(
$"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated."))
using (!_profilingLogger.IsEnabled(Logging.LogLevel.Debug)
? null
: _profilingLogger.DebugDuration<ComponentCollection>($"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<ComponentCollection>(
$"Terminating {componentType.FullName}.",
$"Terminated {componentType.FullName}.",
thresholdMilliseconds: LogThresholdMilliseconds))

using (_profilingLogger.IsEnabled(Logging.LogLevel.Debug) is false
? null
: _profilingLogger.DebugDuration<ComponentCollection>($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
try
{
component.Terminate();
component.DisposeIfDisposable();
await component.TerminateAsync(isRestarting, cancellationToken);
}
catch (Exception ex)
{
Expand Down
22 changes: 10 additions & 12 deletions src/Umbraco.Core/Composing/ComponentCollectionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,33 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Builds a <see cref="ComponentCollection" />.
/// Builds a <see cref="ComponentCollection" />.
/// </summary>
public class
ComponentCollectionBuilder : OrderedCollectionBuilderBase<ComponentCollectionBuilder, ComponentCollection,
IComponent>
public class ComponentCollectionBuilder : OrderedCollectionBuilderBase<ComponentCollectionBuilder, ComponentCollection, IAsyncComponent>
{
private const int LogThresholdMilliseconds = 100;

protected override ComponentCollectionBuilder This => this;

protected override IEnumerable<IComponent> CreateItems(IServiceProvider factory)
protected override IEnumerable<IAsyncComponent> CreateItems(IServiceProvider factory)
{
IProfilingLogger logger = factory.GetRequiredService<IProfilingLogger>();

using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration<ComponentCollectionBuilder>(
$"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created."))
using (logger.IsEnabled(Logging.LogLevel.Debug) is false
? null
: logger.DebugDuration<ComponentCollectionBuilder>($"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<IProfilingLogger>();

using (!logger.IsEnabled(Logging.LogLevel.Debug) ? null : logger.DebugDuration<ComponentCollectionBuilder>(
$"Creating {itemType.FullName}.",
$"Created {itemType.FullName}.",
thresholdMilliseconds: LogThresholdMilliseconds))
using (logger.IsEnabled(Logging.LogLevel.Debug) is false
? null :
logger.DebugDuration<ComponentCollectionBuilder>($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
return base.CreateItem(factory, itemType);
}
Expand Down
23 changes: 14 additions & 9 deletions src/Umbraco.Core/Composing/ComponentComposer.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.DependencyInjection;

namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Provides a base class for composers which compose a component.
/// Provides a composer that appends a component.
/// </summary>
/// <typeparam name="TComponent">The type of the component</typeparam>
/// <typeparam name="TComponent">The type of the component.</typeparam>
/// <remarks>
/// Thanks to this class, a component that does not compose anything can be registered with one line:
/// <code>
/// <![CDATA[
/// public class MyComponentComposer : ComponentComposer<MyComponent> { }
/// ]]>
/// </code>
/// </remarks>
public abstract class ComponentComposer<TComponent> : IComposer
where TComponent : IComponent
where TComponent : IAsyncComponent
{
/// <inheritdoc />
public virtual void Compose(IUmbracoBuilder builder) => builder.Components().Append<TComponent>();

// note: thanks to this class, a component that does not compose anything can be
// registered with one line:
// public class MyComponentComposer : ComponentComposer<MyComponent> { }
public virtual void Compose(IUmbracoBuilder builder)
=> builder.Components().Append<TComponent>();
}
35 changes: 35 additions & 0 deletions src/Umbraco.Core/Composing/IAsyncComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents a component.
/// </summary>
/// <remarks>
/// <para>
/// Components are created by DI and therefore must have a public constructor.
/// </para>
/// <para>
/// All components are terminated in reverse order when Umbraco terminates, and disposable components are disposed.
/// </para>
/// </remarks>
public interface IAsyncComponent
{
/// <summary>
/// Initializes the component.
/// </summary>
/// <param name="isRestarting">If set to <c>true</c> indicates Umbraco is restarting.</param>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the start process has been aborted.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
Task InitializeAsync(bool isRestarting, CancellationToken cancellationToken);

/// <summary>
/// Terminates the component.
/// </summary>
/// <param name="isRestarting">If set to <c>true</c> indicates Umbraco is restarting.</param>
/// <param name="cancellationToken">The cancellation token. Cancellation indicates that the shutdown process should no longer be graceful.</param>
/// <returns>
/// A <see cref="Task" /> representing the asynchronous operation.
/// </returns>
Task TerminateAsync(bool isRestarting, CancellationToken cancellationToken);
}
38 changes: 21 additions & 17 deletions src/Umbraco.Core/Composing/IComponent.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents a component.
/// </summary>
/// <remarks>
/// <para>Components are created by DI and therefore must have a public constructor.</para>
/// <para>
/// All components are terminated in reverse order when Umbraco terminates, and
/// disposable components are disposed.
/// </para>
/// <para>
/// The Dispose method may be invoked more than once, and components
/// should ensure they support this.
/// </para>
/// </remarks>
public interface IComponent
/// <inheritdoc />
[Obsolete("Use IAsyncComponent instead. This interface will be removed in a future version.")]
public interface IComponent : IAsyncComponent
{
/// <summary>
/// Initializes the component.
/// Initializes the component.
/// </summary>
void Initialize();

/// <summary>
/// Terminates the component.
/// Terminates the component.
/// </summary>
void Terminate();

/// <inheritdoc />
Task IAsyncComponent.InitializeAsync(bool isRestarting, CancellationToken cancellationToken)
{
Initialize();

return Task.CompletedTask;
}

/// <inheritdoc />
Task IAsyncComponent.TerminateAsync(bool isRestarting, CancellationToken cancellationToken)
{
Terminate();

return Task.CompletedTask;
}
}
23 changes: 23 additions & 0 deletions src/Umbraco.Core/Composing/RuntimeAsyncComponentBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Umbraco.Cms.Core.Services;

namespace Umbraco.Cms.Core.Composing;

/// <inheritdoc />
/// <remarks>
/// By default, the component will not execute if Umbraco is restarting or the runtime level is not <see cref="RuntimeLevel.Run" />.
/// </remarks>
public abstract class RuntimeAsyncComponentBase : AsyncComponentBase
{
private readonly IRuntimeState _runtimeState;

/// <summary>
/// Initializes a new instance of the <see cref="RuntimeAsyncComponentBase" /> class.
/// </summary>
/// <param name="runtimeState">State of the Umbraco runtime.</param>
protected RuntimeAsyncComponentBase(IRuntimeState runtimeState)
=> _runtimeState = runtimeState;

/// <inheritdoc />
protected override bool CanExecute(bool isRestarting)
=> base.CanExecute(isRestarting) && _runtimeState.Level == RuntimeLevel.Run;
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static partial class UmbracoBuilderExtensions
/// The builder.
/// </returns>
public static IUmbracoBuilder AddComponent<T>(this IUmbracoBuilder builder)
where T : IComponent
where T : IAsyncComponent
{
builder.Components().Append<T>();

Expand Down
3 changes: 2 additions & 1 deletion src/Umbraco.Core/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ public static class ObjectExtensions
public static IEnumerable<T> AsEnumerableOfOne<T>(this T input) => Enumerable.Repeat(input, 1);

/// <summary>
/// Disposes the object if it implements <see cref="IDisposable" />.
/// </summary>
/// <param name="input"></param>
/// <param name="input">The object.</param>
public static void DisposeIfDisposable(this object input)
{
if (input is IDisposable disposable)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,26 @@
namespace Umbraco.Cms.Core.Notifications;

/// <summary>
/// Notification that occurs at the very end of the Umbraco boot process (after all <see cref="Composing.IComponent" />s are
/// initialized).
/// Notification that occurs at the very end of the Umbraco boot process (after all components are initialized).
/// </summary>
public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoApplicationStartingNotification" /> class.
/// </summary>
/// <seealso cref="IUmbracoApplicationLifetimeNotification" />
public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification
/// <param name="runtimeLevel">The runtime level</param>
/// <param name="isRestarting">Indicates whether Umbraco is restarting.</param>
public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting)
{
/// <summary>
/// Initializes a new instance of the <see cref="UmbracoApplicationStartingNotification" /> class.
/// </summary>
/// <param name="runtimeLevel">The runtime level</param>
/// <param name="isRestarting">Indicates whether Umbraco is restarting.</param>
public UmbracoApplicationStartingNotification(RuntimeLevel runtimeLevel, bool isRestarting)
{
RuntimeLevel = runtimeLevel;
IsRestarting = isRestarting;
}
RuntimeLevel = runtimeLevel;
IsRestarting = isRestarting;
}

/// <summary>
/// Gets the runtime level.
/// Gets the runtime level.
/// </summary>
/// <value>
/// The runtime level.
/// The runtime level.
/// </value>
public RuntimeLevel RuntimeLevel { get; }

Expand Down
Loading
Loading