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 3 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;
}
48 changes: 25 additions & 23 deletions src/Umbraco.Core/Composing/ComponentCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,61 @@
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);
await component.DisposeAsyncIfDisposable();
ronaldbarendse marked this conversation as resolved.
Show resolved Hide resolved
}
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>();
}
38 changes: 38 additions & 0 deletions src/Umbraco.Core/Composing/IAsyncComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
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>
ronaldbarendse marked this conversation as resolved.
Show resolved Hide resolved
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);
}
43 changes: 30 additions & 13 deletions src/Umbraco.Core/Composing/IComponent.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,45 @@
namespace Umbraco.Cms.Core.Composing;

/// <summary>
/// Represents a component.
/// 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>
/// <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>
ronaldbarendse marked this conversation as resolved.
Show resolved Hide resolved
public interface IComponent
[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
19 changes: 18 additions & 1 deletion src/Umbraco.Core/Extensions/ObjectExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Umbraco.

Check notice on line 1 in src/Umbraco.Core/Extensions/ObjectExtensions.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (contrib)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 7.24 to 7.00, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.

Check notice on line 1 in src/Umbraco.Core/Extensions/ObjectExtensions.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (contrib)

ℹ Getting worse: Primitive Obsession

The ratio of primitive types in function arguments increases from 58.33% to 59.46%, threshold = 30.0%. The functions in this file have too many primitive types (e.g. int, double, float) in their function argument lists. Using many primitive types lead to the code smell Primitive Obsession. Avoid adding more primitive arguments.
// See LICENSE for more details.

using System.Collections;
Expand Down Expand Up @@ -41,8 +41,9 @@
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 All @@ -51,6 +52,22 @@
}
}

/// <summary>
/// Disposes the object if it implements <see cref="IAsyncDisposable" /> or <see cref="IDisposable" />.
/// </summary>
/// <param name="input">The object.</param>
public static async Task DisposeAsyncIfDisposable(this object input)
{
if (input is IAsyncDisposable disposable)
{
await disposable.DisposeAsync();
}
else
{
input.DisposeIfDisposable();
}
}

/// <summary>
/// Provides a shortcut way of safely casting an input when you cannot guarantee the <typeparamref name="T" /> is
/// an instance type (i.e., when the C# AS keyword is not applicable).
Expand Down
Loading
Loading