diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 6caeadd0e5..9dc6f9457f 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -16,7 +16,7 @@ This document gives you a quick overview on how to get started, we will link to
## Guidelines for contributions we welcome
-Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valueable time.
+Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valuable time.
We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes.
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index 51d7e3b8d0..35e79d8127 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -25,7 +25,7 @@
-
+
diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index 434fe812ef..565d693979 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -22,11 +22,11 @@
not want this to happen as the alpha of the next major is, really, the next major already.
-->
-
+
-
-
+
+
diff --git a/build/build.ps1 b/build/build.ps1
index ec53199251..dafae2665a 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -452,9 +452,22 @@
if ($this.OnError()) { return }
$this.PrepareAzureGallery()
if ($this.OnError()) { return }
+ $this.PostPackageHook()
+ if ($this.OnError()) { return }
Write-Host "Done"
})
+ $ubuild.DefineMethod("PostPackageHook",
+ {
+ # run hook
+ if ($this.HasMethod("PostPackage"))
+ {
+ Write-Host "Run PostPackage hook"
+ $this.PostPackage();
+ if (-not $?) { throw "Failed to run hook." }
+ }
+ })
+
# ################################################################
# RUN
# ################################################################
diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs
index b5af335791..ce40bd9baa 100644
--- a/src/SolutionInfo.cs
+++ b/src/SolutionInfo.cs
@@ -19,4 +19,4 @@ using System.Resources;
// these are FYI and changed automatically
[assembly: AssemblyFileVersion("8.0.0")]
-[assembly: AssemblyInformationalVersion("8.0.0-alpha.52")]
+[assembly: AssemblyInformationalVersion("8.0.0-alpha.58")]
diff --git a/src/Umbraco.Core/Cache/CacheHelper.cs b/src/Umbraco.Core/Cache/CacheHelper.cs
index f99b1e847b..cd2225ae9d 100644
--- a/src/Umbraco.Core/Cache/CacheHelper.cs
+++ b/src/Umbraco.Core/Cache/CacheHelper.cs
@@ -4,27 +4,10 @@ using System.Web;
namespace Umbraco.Core.Cache
{
///
- /// Class that is exposed by the ApplicationContext for application wide caching purposes
+ /// Represents the application-wide caches.
///
public class CacheHelper
{
- public static CacheHelper NoCache { get; } = new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance));
-
- ///
- /// Creates a cache helper with disabled caches
- ///
- ///
- ///
- /// Good for unit testing
- ///
- public static CacheHelper CreateDisabledCacheHelper()
- {
- // do *not* return NoCache
- // NoCache is a special instance that is detected by RepositoryBase and disables all cache policies
- // CreateDisabledCacheHelper is used in tests to use no cache, *but* keep all cache policies
- return new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance));
- }
-
///
/// Initializes a new instance for use in the web
///
@@ -40,7 +23,6 @@ namespace Umbraco.Core.Cache
///
/// Initializes a new instance for use in the web
///
- ///
public CacheHelper(System.Web.Caching.Cache cache)
: this(
new HttpRuntimeCacheProvider(cache),
@@ -50,30 +32,39 @@ namespace Umbraco.Core.Cache
{
}
-
///
/// Initializes a new instance based on the provided providers
///
- ///
- ///
- ///
- ///
public CacheHelper(
IRuntimeCacheProvider httpCacheProvider,
ICacheProvider staticCacheProvider,
ICacheProvider requestCacheProvider,
IsolatedRuntimeCache isolatedCacheManager)
{
- if (httpCacheProvider == null) throw new ArgumentNullException("httpCacheProvider");
- if (staticCacheProvider == null) throw new ArgumentNullException("staticCacheProvider");
- if (requestCacheProvider == null) throw new ArgumentNullException("requestCacheProvider");
- if (isolatedCacheManager == null) throw new ArgumentNullException("isolatedCacheManager");
- RuntimeCache = httpCacheProvider;
- StaticCache = staticCacheProvider;
- RequestCache = requestCacheProvider;
- IsolatedRuntimeCache = isolatedCacheManager;
+ RuntimeCache = httpCacheProvider ?? throw new ArgumentNullException(nameof(httpCacheProvider));
+ StaticCache = staticCacheProvider ?? throw new ArgumentNullException(nameof(staticCacheProvider));
+ RequestCache = requestCacheProvider ?? throw new ArgumentNullException(nameof(requestCacheProvider));
+ IsolatedRuntimeCache = isolatedCacheManager ?? throw new ArgumentNullException(nameof(isolatedCacheManager));
}
+ ///
+ /// Gets the special disabled instance.
+ ///
+ ///
+ /// When used by repositories, all cache policies apply, but the underlying caches do not cache anything.
+ /// Used by tests.
+ ///
+ public static CacheHelper Disabled { get; } = new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance));
+
+ ///
+ /// Gets the special no-cache instance.
+ ///
+ ///
+ /// When used by repositories, all cache policies are bypassed.
+ /// Used by repositories that do no cache.
+ ///
+ public static CacheHelper NoCache { get; } = new CacheHelper(NullCacheProvider.Instance, NullCacheProvider.Instance, NullCacheProvider.Instance, new IsolatedRuntimeCache(_ => NullCacheProvider.Instance));
+
///
/// Returns the current Request cache
///
diff --git a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs
index 11ac05844b..8bae755149 100644
--- a/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs
+++ b/src/Umbraco.Core/Cache/CacheRefresherCollectionBuilder.cs
@@ -1,15 +1,9 @@
-using System.Collections.Generic;
-using LightInject;
-using Umbraco.Core.Composing;
+using Umbraco.Core.Composing;
namespace Umbraco.Core.Cache
{
public class CacheRefresherCollectionBuilder : LazyCollectionBuilderBase
{
- public CacheRefresherCollectionBuilder(IServiceContainer container)
- : base(container)
- { }
-
protected override CacheRefresherCollectionBuilder This => this;
}
}
diff --git a/src/Umbraco.Core/Components/AuditEventsComponent.cs b/src/Umbraco.Core/Components/AuditEventsComponent.cs
index 134aa18414..08d4702afa 100644
--- a/src/Umbraco.Core/Components/AuditEventsComponent.cs
+++ b/src/Umbraco.Core/Components/AuditEventsComponent.cs
@@ -12,11 +12,36 @@ using Umbraco.Core.Services.Implement;
namespace Umbraco.Core.Components
{
- public sealed class AuditEventsComponent : UmbracoComponentBase, IUmbracoCoreComponent
+ public sealed class AuditEventsComponent : IComponent
{
- private IAuditService _auditService;
- private IUserService _userService;
- private IEntityService _entityService;
+ private readonly IAuditService _auditService;
+ private readonly IUserService _userService;
+ private readonly IEntityService _entityService;
+
+ public AuditEventsComponent(IAuditService auditService, IUserService userService, IEntityService entityService)
+ {
+ _auditService = auditService;
+ _userService = userService;
+ _entityService = entityService;
+ }
+
+ public void Initialize()
+ {
+ UserService.SavedUserGroup += OnSavedUserGroupWithUsers;
+
+ UserService.SavedUser += OnSavedUser;
+ UserService.DeletedUser += OnDeletedUser;
+ UserService.UserGroupPermissionsAssigned += UserGroupPermissionAssigned;
+
+ MemberService.Saved += OnSavedMember;
+ MemberService.Deleted += OnDeletedMember;
+ MemberService.AssignedRoles += OnAssignedRoles;
+ MemberService.RemovedRoles += OnRemovedRoles;
+ MemberService.Exported += OnMemberExported;
+ }
+
+ public void Terminate()
+ { }
private IUser CurrentPerformingUser
{
@@ -46,25 +71,6 @@ namespace Umbraco.Core.Components
}
}
- public void Initialize(IAuditService auditService, IUserService userService, IEntityService entityService)
- {
- _auditService = auditService;
- _userService = userService;
- _entityService = entityService;
-
- UserService.SavedUserGroup += OnSavedUserGroupWithUsers;
-
- UserService.SavedUser += OnSavedUser;
- UserService.DeletedUser += OnDeletedUser;
- UserService.UserGroupPermissionsAssigned += UserGroupPermissionAssigned;
-
- MemberService.Saved += OnSavedMember;
- MemberService.Deleted += OnDeletedMember;
- MemberService.AssignedRoles += OnAssignedRoles;
- MemberService.RemovedRoles += OnRemovedRoles;
- MemberService.Exported += OnMemberExported;
- }
-
private string FormatEmail(IMember member)
{
return member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>";
diff --git a/src/Umbraco.Core/Components/AuditEventsComposer.cs b/src/Umbraco.Core/Components/AuditEventsComposer.cs
new file mode 100644
index 0000000000..692cb6c6dd
--- /dev/null
+++ b/src/Umbraco.Core/Components/AuditEventsComposer.cs
@@ -0,0 +1,5 @@
+namespace Umbraco.Core.Components
+{
+ public sealed class AuditEventsComposer : ComponentComposer, ICoreComposer
+ { }
+}
diff --git a/src/Umbraco.Core/Components/BootLoader.cs b/src/Umbraco.Core/Components/BootLoader.cs
deleted file mode 100644
index fd292990c8..0000000000
--- a/src/Umbraco.Core/Components/BootLoader.cs
+++ /dev/null
@@ -1,368 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
-using System.Text;
-using LightInject;
-using Umbraco.Core.Collections;
-using Umbraco.Core.Exceptions;
-using Umbraco.Core.Logging;
-using Umbraco.Core.Scoping;
-
-namespace Umbraco.Core.Components
-{
- // note: this class is NOT thread-safe in any ways
-
- internal class BootLoader
- {
- private readonly IServiceContainer _container;
- private readonly ProfilingLogger _proflog;
- private readonly ILogger _logger;
- private IUmbracoComponent[] _components;
- private bool _booted;
-
- private const int LogThresholdMilliseconds = 100;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The application container.
- public BootLoader(IServiceContainer container)
- {
- _container = container ?? throw new ArgumentNullException(nameof(container));
- _proflog = container.GetInstance();
- _logger = container.GetInstance();
- }
-
- private class EnableInfo
- {
- public bool Enabled;
- public int Weight = -1;
- }
-
- public void Boot(IEnumerable componentTypes, RuntimeLevel level)
- {
- if (_booted) throw new InvalidOperationException("Can not boot, has already booted.");
-
- var orderedComponentTypes = PrepareComponentTypes(componentTypes, level);
-
- InstanciateComponents(orderedComponentTypes);
- ComposeComponents(level);
-
- using (var scope = _container.GetInstance().CreateScope())
- {
- InitializeComponents();
- scope.Complete();
- }
-
- // rejoice!
- _booted = true;
- }
-
- private IEnumerable PrepareComponentTypes(IEnumerable componentTypes, RuntimeLevel level)
- {
- using (_proflog.DebugDuration("Preparing component types.", "Prepared component types."))
- {
- return PrepareComponentTypes2(componentTypes, level);
- }
- }
-
- private IEnumerable PrepareComponentTypes2(IEnumerable componentTypes, RuntimeLevel level)
- {
- // create a list, remove those that cannot be enabled due to runtime level
- var componentTypeList = componentTypes
- .Where(x =>
- {
- // use the min level specified by the attribute if any
- // otherwise, user components have Run min level, anything else is Unknown (always run)
- var attr = x.GetCustomAttribute();
- var minLevel = attr?.MinLevel ?? (x.Implements() ? RuntimeLevel.Run : RuntimeLevel.Unknown);
- return level >= minLevel;
- })
- .ToList();
-
- // cannot remove that one - ever
- if (componentTypeList.Contains(typeof(UmbracoCoreComponent)) == false)
- componentTypeList.Add(typeof(UmbracoCoreComponent));
-
- // enable or disable components
- EnableDisableComponents(componentTypeList);
-
- // sort the components according to their dependencies
- var requirements = new Dictionary>();
- foreach (var type in componentTypeList) requirements[type] = null;
- foreach (var type in componentTypeList)
- {
- GatherRequirementsFromRequireAttribute(type, componentTypeList, requirements);
- GatherRequirementsFromRequiredAttribute(type, componentTypeList, requirements);
- }
-
- // only for debugging, this is verbose
- //_logger.Debug(GetComponentsReport(requirements));
-
- // sort components
- var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value);
- graph.AddItems(requirements);
- List sortedComponentTypes;
- try
- {
- sortedComponentTypes = graph.GetSortedItems().Select(x => x.Key).ToList();
- }
- catch (Exception e)
- {
- // in case of an error, force-dump everything to log
- _logger.Info("Component Report:\r\n{ComponentReport}", GetComponentsReport(requirements));
- _logger.Error(e, "Failed to sort compontents.");
- throw;
- }
-
- // bit verbose but should help for troubleshooting
- var text = "Ordered Components: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComponentTypes) + Environment.NewLine;
- Console.WriteLine(text);
- _logger.Debug("Ordered Components: {SortedComponentTypes}", sortedComponentTypes);
-
- return sortedComponentTypes;
- }
-
- private static string GetComponentsReport(Dictionary> requirements)
- {
- var text = new StringBuilder();
- text.AppendLine("Components & Dependencies:");
- text.AppendLine();
-
- foreach (var kvp in requirements)
- {
- var type = kvp.Key;
-
- text.AppendLine(type.FullName);
- foreach (var attribute in type.GetCustomAttributes())
- text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
- ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
- : ""));
- foreach (var attribute in type.GetCustomAttributes())
- text.AppendLine(" -< " + attribute.RequiringType);
- foreach (var i in type.GetInterfaces())
- {
- text.AppendLine(" : " + i.FullName);
- foreach (var attribute in i.GetCustomAttributes())
- text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
- ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
- : ""));
- foreach (var attribute in i.GetCustomAttributes())
- text.AppendLine(" -< " + attribute.RequiringType);
- }
- if (kvp.Value != null)
- foreach (var t in kvp.Value)
- text.AppendLine(" = " + t);
- text.AppendLine();
- }
- text.AppendLine("/");
- text.AppendLine();
- return text.ToString();
- }
-
- private static void EnableDisableComponents(ICollection types)
- {
- var enabled = new Dictionary();
-
- // process the enable/disable attributes
- // these two attributes are *not* inherited and apply to *classes* only (not interfaces).
- // remote declarations (when a component enables/disables *another* component)
- // have priority over local declarations (when a component disables itself) so that
- // ppl can enable components that, by default, are disabled.
- // what happens in case of conflicting remote declarations is unspecified. more
- // precisely, the last declaration to be processed wins, but the order of the
- // declarations depends on the type finder and is unspecified.
- foreach (var componentType in types)
- {
- foreach (var attr in componentType.GetCustomAttributes())
- {
- var type = attr.EnabledType ?? componentType;
- if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo();
- var weight = type == componentType ? 1 : 2;
- if (enableInfo.Weight > weight) continue;
-
- enableInfo.Enabled = true;
- enableInfo.Weight = weight;
- }
- foreach (var attr in componentType.GetCustomAttributes())
- {
- var type = attr.DisabledType ?? componentType;
- if (type == typeof(UmbracoCoreComponent)) throw new InvalidOperationException("Cannot disable UmbracoCoreComponent.");
- if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo();
- var weight = type == componentType ? 1 : 2;
- if (enableInfo.Weight > weight) continue;
-
- enableInfo.Enabled = false;
- enableInfo.Weight = weight;
- }
- }
-
- // remove components that end up being disabled
- foreach (var kvp in enabled.Where(x => x.Value.Enabled == false))
- types.Remove(kvp.Key);
- }
-
- private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements)
- {
- // get 'require' attributes
- // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
- var requireAttributes = type
- .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces
- .Concat(type.GetCustomAttributes()); // those marking the component
-
- // what happens in case of conflicting attributes (different strong/weak for same type) is not specified.
- foreach (var attr in requireAttributes)
- {
- if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below)
-
- // requiring an interface = require any enabled component implementing that interface
- // unless strong, and then require at least one enabled component implementing that interface
- if (attr.RequiredType.IsInterface)
- {
- var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList();
- if (implems.Count > 0)
- {
- if (requirements[type] == null) requirements[type] = new List();
- requirements[type].AddRange(implems);
- }
- else if (attr.Weak == false) // if explicitely set to !weak, is strong, else is weak
- throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
- }
- // requiring a class = require that the component is enabled
- // unless weak, and then requires it if it is enabled
- else
- {
- if (types.Contains(attr.RequiredType))
- {
- if (requirements[type] == null) requirements[type] = new List();
- requirements[type].Add(attr.RequiredType);
- }
- else if (attr.Weak != true) // if not explicitely set to weak, is strong
- throw new Exception($"Broken component dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
- }
- }
- }
-
- private static void GatherRequirementsFromRequiredAttribute(Type type, ICollection types, IDictionary> requirements)
- {
- // get 'required' attributes
- // fixme explain
- var requiredAttributes = type
- .GetInterfaces().SelectMany(x => x.GetCustomAttributes())
- .Concat(type.GetCustomAttributes());
-
- foreach (var attr in requiredAttributes)
- {
- if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below)
-
- if (attr.RequiringType.IsInterface)
- {
- var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList();
- foreach (var implem in implems)
- {
- if (requirements[implem] == null) requirements[implem] = new List();
- requirements[implem].Add(type);
- }
- }
- else
- {
- if (types.Contains(attr.RequiringType))
- {
- if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List();
- requirements[attr.RequiringType].Add(type);
- }
- }
- }
- }
-
- private void InstanciateComponents(IEnumerable types)
- {
- using (_proflog.DebugDuration("Instanciating components.", "Instanciated components."))
- {
- _components = types.Select(x => (IUmbracoComponent) Activator.CreateInstance(x)).ToArray();
- }
- }
-
- private void ComposeComponents(RuntimeLevel level)
- {
- using (_proflog.DebugDuration($"Composing components. (log when >{LogThresholdMilliseconds}ms)", "Composed components."))
- {
- var composition = new Composition(_container, level);
- foreach (var component in _components)
- {
- var componentType = component.GetType();
- using (_proflog.DebugDuration($"Composing {componentType.FullName}.", $"Composed {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
- {
- component.Compose(composition);
- }
- }
- }
- }
-
- private void InitializeComponents()
- {
- // use a container scope to ensure that PerScope instances are disposed
- // components that require instances that should not survive should register them with PerScope lifetime
- using (_proflog.DebugDuration($"Initializing components. (log when >{LogThresholdMilliseconds}ms)", "Initialized components."))
- using (_container.BeginScope())
- {
- foreach (var component in _components)
- {
- var componentType = component.GetType();
- var initializers = componentType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
- .Where(x => x.Name == "Initialize" && x.IsGenericMethod == false);
- using (_proflog.DebugDuration($"Initializing {componentType.FullName}.", $"Initialised {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
- {
- foreach (var initializer in initializers)
- {
- var parameters = initializer.GetParameters()
- .Select(x => GetParameter(componentType, x.ParameterType))
- .ToArray();
- initializer.Invoke(component, parameters);
- }
- }
- }
- }
- }
-
- private object GetParameter(Type componentType, Type parameterType)
- {
- object param;
-
- try
- {
- param = _container.TryGetInstance(parameterType);
- }
- catch (Exception e)
- {
- throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}.", e);
- }
-
- if (param == null) throw new BootFailedException($"Could not get parameter of type {parameterType.FullName} for component {componentType.FullName}.");
- return param;
- }
-
- public void Terminate()
- {
- if (_booted == false)
- {
- _proflog.Logger.Warn("Cannot terminate, has not booted.");
- return;
- }
-
- using (_proflog.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated."))
- {
- for (var i = _components.Length - 1; i >= 0; i--) // terminate components in reverse order
- {
- var component = _components[i];
- var componentType = component.GetType();
- using (_proflog.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
- {
- component.Terminate();
- }
- }
- }
- }
- }
-}
diff --git a/src/Umbraco.Core/Components/ComponentCollection.cs b/src/Umbraco.Core/Components/ComponentCollection.cs
new file mode 100644
index 0000000000..4fa81b9760
--- /dev/null
+++ b/src/Umbraco.Core/Components/ComponentCollection.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Logging;
+
+namespace Umbraco.Core.Components
+{
+ ///
+ /// Represents the collection of implementations.
+ ///
+ public class ComponentCollection : BuilderCollectionBase
+ {
+ private const int LogThresholdMilliseconds = 100;
+
+ private readonly IProfilingLogger _logger;
+
+ public ComponentCollection(IEnumerable items, IProfilingLogger logger)
+ : base(items)
+ {
+ _logger = logger;
+ }
+
+ public void Initialize()
+ {
+ using (_logger.DebugDuration($"Initializing. (log components when >{LogThresholdMilliseconds}ms)", "Initialized."))
+ {
+ foreach (var component in this.Reverse()) // terminate components in reverse order
+ {
+ var componentType = component.GetType();
+ using (_logger.DebugDuration($"Initializing {componentType.FullName}.", $"Initialized {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
+ {
+ component.Initialize();
+ }
+ }
+ }
+ }
+
+ public void Terminate()
+ {
+ using (_logger.DebugDuration($"Terminating. (log components when >{LogThresholdMilliseconds}ms)", "Terminated."))
+ {
+ foreach (var component in this.Reverse()) // terminate components in reverse order
+ {
+ var componentType = component.GetType();
+ using (_logger.DebugDuration($"Terminating {componentType.FullName}.", $"Terminated {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
+ {
+ component.Terminate();
+ component.DisposeIfDisposable();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Components/ComponentCollectionBuilder.cs b/src/Umbraco.Core/Components/ComponentCollectionBuilder.cs
new file mode 100644
index 0000000000..584de7a8f2
--- /dev/null
+++ b/src/Umbraco.Core/Components/ComponentCollectionBuilder.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using Umbraco.Core.Composing;
+using Umbraco.Core.Logging;
+
+namespace Umbraco.Core.Components
+{
+ ///
+ /// Builds a .
+ ///
+ public class ComponentCollectionBuilder : OrderedCollectionBuilderBase
+ {
+ private const int LogThresholdMilliseconds = 100;
+
+ private IProfilingLogger _logger;
+
+ public ComponentCollectionBuilder()
+ { }
+
+ protected override ComponentCollectionBuilder This => this;
+
+ protected override IEnumerable CreateItems(IFactory factory)
+ {
+ _logger = factory.GetInstance();
+
+ using (_logger.DebugDuration($"Creating components. (log when >{LogThresholdMilliseconds}ms)", "Created."))
+ {
+ return base.CreateItems(factory);
+ }
+ }
+
+ protected override IComponent CreateItem(IFactory factory, Type itemType)
+ {
+ using (_logger.DebugDuration($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
+ {
+ return base.CreateItem(factory, itemType);
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Components/ComponentComposer.cs b/src/Umbraco.Core/Components/ComponentComposer.cs
new file mode 100644
index 0000000000..792790c42f
--- /dev/null
+++ b/src/Umbraco.Core/Components/ComponentComposer.cs
@@ -0,0 +1,20 @@
+namespace Umbraco.Core.Components
+{
+ ///
+ /// Provides a base class for composers which compose a component.
+ ///
+ /// The type of the component
+ public abstract class ComponentComposer : IComposer
+ where TComponent : IComponent
+ {
+ ///
+ public virtual void Compose(Composition composition)
+ {
+ composition.Components().Append();
+ }
+
+ // note: thanks to this class, a component that does not compose anything can be
+ // registered with one line:
+ // public class MyComponentComposer : ComponentComposer { }
+ }
+}
diff --git a/src/Umbraco.Core/Components/ComposeAfterAttribute.cs b/src/Umbraco.Core/Components/ComposeAfterAttribute.cs
new file mode 100644
index 0000000000..a8fdfaa92b
--- /dev/null
+++ b/src/Umbraco.Core/Components/ComposeAfterAttribute.cs
@@ -0,0 +1,59 @@
+using System;
+
+namespace Umbraco.Core.Components
+{
+ ///
+ /// Indicates that a composer requires another composer.
+ ///
+ ///
+ /// This attribute is *not* inherited. This means that a composer class inheriting from
+ /// another composer class does *not* inherit its requirements. However, the runtime checks
+ /// the *interfaces* of every composer for their requirements, so requirements declared on
+ /// interfaces are inherited by every composer class implementing the interface.
+ /// When targeting a class, indicates a dependency on the composer which must be enabled,
+ /// unless the requirement has explicitly been declared as weak (and then, only if the composer
+ /// is enabled).
+ /// When targeting an interface, indicates a dependency on enabled composers implementing
+ /// the interface. It could be no composer at all, unless the requirement has explicitly been
+ /// declared as strong (and at least one composer must be enabled).
+ ///
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)]
+ public sealed class ComposeAfterAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type of the required composer.
+ public ComposeAfterAttribute(Type requiredType)
+ {
+ if (typeof(IComposer).IsAssignableFrom(requiredType) == false)
+ throw new ArgumentException($"Type {requiredType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}.");
+ RequiredType = requiredType;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type of the required composer.
+ /// A value indicating whether the requirement is weak.
+ public ComposeAfterAttribute(Type requiredType, bool weak)
+ : this(requiredType)
+ {
+ Weak = weak;
+ }
+
+ ///
+ /// Gets the required type.
+ ///
+ public Type RequiredType { get; }
+
+ ///
+ /// Gets a value indicating whether the requirement is weak.
+ ///
+ /// Returns true if the requirement is weak (requires the other composer if it
+ /// is enabled), false if the requirement is strong (requires the other composer to be
+ /// enabled), and null if unspecified, in which case it is strong for classes and weak for
+ /// interfaces.
+ public bool? Weak { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Components/ComposeBeforeAttribute.cs b/src/Umbraco.Core/Components/ComposeBeforeAttribute.cs
new file mode 100644
index 0000000000..17065d1676
--- /dev/null
+++ b/src/Umbraco.Core/Components/ComposeBeforeAttribute.cs
@@ -0,0 +1,40 @@
+using System;
+
+namespace Umbraco.Core.Components
+{
+ ///
+ /// Indicates that a component is required by another composer.
+ ///
+ ///
+ /// This attribute is *not* inherited. This means that a composer class inheriting from
+ /// another composer class does *not* inherit its requirements. However, the runtime checks
+ /// the *interfaces* of every composer for their requirements, so requirements declared on
+ /// interfaces are inherited by every composer class implementing the interface.
+ /// When targeting a class, indicates a dependency on the composer which must be enabled,
+ /// unless the requirement has explicitly been declared as weak (and then, only if the composer
+ /// is enabled).
+ /// When targeting an interface, indicates a dependency on enabled composers implementing
+ /// the interface. It could be no composer at all, unless the requirement has explicitly been
+ /// declared as strong (and at least one composer must be enabled).
+ ///
+
+ [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = true, Inherited = false)]
+ public sealed class ComposeBeforeAttribute : Attribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The type of the required composer.
+ public ComposeBeforeAttribute(Type requiringType)
+ {
+ if (typeof(IComposer).IsAssignableFrom(requiringType) == false)
+ throw new ArgumentException($"Type {requiringType.FullName} is invalid here because it does not implement {typeof(IComposer).FullName}.");
+ RequiringType = requiringType;
+ }
+
+ ///
+ /// Gets the required type.
+ ///
+ public Type RequiringType { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Components/Composers.cs b/src/Umbraco.Core/Components/Composers.cs
new file mode 100644
index 0000000000..1c836e9e5c
--- /dev/null
+++ b/src/Umbraco.Core/Components/Composers.cs
@@ -0,0 +1,294 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Umbraco.Core.Collections;
+using Umbraco.Core.Logging;
+
+namespace Umbraco.Core.Components
+{
+ // note: this class is NOT thread-safe in any ways
+
+ ///
+ /// Handles the composers.
+ ///
+ public class Composers
+ {
+ private readonly Composition _composition;
+ private readonly IProfilingLogger _logger;
+ private readonly IEnumerable _composerTypes;
+
+ private const int LogThresholdMilliseconds = 100;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The composition.
+ /// The composer types.
+ /// A profiling logger.
+ public Composers(Composition composition, IEnumerable composerTypes, IProfilingLogger logger)
+ {
+ _composition = composition ?? throw new ArgumentNullException(nameof(composition));
+ _composerTypes = composerTypes ?? throw new ArgumentNullException(nameof(composerTypes));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ private class EnableInfo
+ {
+ public bool Enabled;
+ public int Weight = -1;
+ }
+
+ ///
+ /// Instantiates and composes the composers.
+ ///
+ public void Compose()
+ {
+ // make sure it is there
+ _composition.WithCollectionBuilder();
+
+ IEnumerable orderedComposerTypes;
+
+ using (_logger.DebugDuration("Preparing composer types.", "Prepared composer types."))
+ {
+ orderedComposerTypes = PrepareComposerTypes();
+ }
+
+ var composers = InstantiateComposers(orderedComposerTypes);
+
+ using (_logger.DebugDuration($"Composing composers. (log when >{LogThresholdMilliseconds}ms)", "Composed composers."))
+ {
+ foreach (var composer in composers)
+ {
+ var componentType = composer.GetType();
+ using (_logger.DebugDuration($"Composing {componentType.FullName}.", $"Composed {componentType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
+ {
+ composer.Compose(_composition);
+ }
+ }
+ }
+ }
+
+ private IEnumerable PrepareComposerTypes()
+ {
+ // create a list, remove those that cannot be enabled due to runtime level
+ var composerTypeList = _composerTypes
+ .Where(x =>
+ {
+ // use the min level specified by the attribute if any
+ // otherwise, user composers have Run min level, anything else is Unknown (always run)
+ var attr = x.GetCustomAttribute();
+ var minLevel = attr?.MinLevel ?? (x.Implements() ? RuntimeLevel.Run : RuntimeLevel.Unknown);
+ return _composition.RuntimeState.Level >= minLevel;
+ })
+ .ToList();
+
+ // enable or disable composers
+ EnableDisableComposers(composerTypeList);
+
+ // sort the composers according to their dependencies
+ var requirements = new Dictionary>();
+ foreach (var type in composerTypeList) requirements[type] = null;
+ foreach (var type in composerTypeList)
+ {
+ GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements);
+ GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements);
+ }
+
+ // only for debugging, this is verbose
+ //_logger.Debug(GetComposersReport(requirements));
+
+ // sort composers
+ var graph = new TopoGraph>>(kvp => kvp.Key, kvp => kvp.Value);
+ graph.AddItems(requirements);
+ List sortedComposerTypes;
+ try
+ {
+ sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList();
+ }
+ catch (Exception e)
+ {
+ // in case of an error, force-dump everything to log
+ _logger.Info("Composer Report:\r\n{ComposerReport}", GetComposersReport(requirements));
+ _logger.Error(e, "Failed to sort composers.");
+ throw;
+ }
+
+ // bit verbose but should help for troubleshooting
+ //var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine;
+ _logger.Debug("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes);
+
+ return sortedComposerTypes;
+ }
+
+ private static string GetComposersReport(Dictionary> requirements)
+ {
+ var text = new StringBuilder();
+ text.AppendLine("Composers & Dependencies:");
+ text.AppendLine();
+
+ foreach (var kvp in requirements)
+ {
+ var type = kvp.Key;
+
+ text.AppendLine(type.FullName);
+ foreach (var attribute in type.GetCustomAttributes())
+ text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
+ ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
+ : ""));
+ foreach (var attribute in type.GetCustomAttributes())
+ text.AppendLine(" -< " + attribute.RequiringType);
+ foreach (var i in type.GetInterfaces())
+ {
+ text.AppendLine(" : " + i.FullName);
+ foreach (var attribute in i.GetCustomAttributes())
+ text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
+ ? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
+ : ""));
+ foreach (var attribute in i.GetCustomAttributes())
+ text.AppendLine(" -< " + attribute.RequiringType);
+ }
+ if (kvp.Value != null)
+ foreach (var t in kvp.Value)
+ text.AppendLine(" = " + t);
+ text.AppendLine();
+ }
+ text.AppendLine("/");
+ text.AppendLine();
+ return text.ToString();
+ }
+
+ private static void EnableDisableComposers(ICollection types)
+ {
+ var enabled = new Dictionary();
+
+ // process the enable/disable attributes
+ // these two attributes are *not* inherited and apply to *classes* only (not interfaces).
+ // remote declarations (when a composer enables/disables *another* composer)
+ // have priority over local declarations (when a composer disables itself) so that
+ // ppl can enable composers that, by default, are disabled.
+ // what happens in case of conflicting remote declarations is unspecified. more
+ // precisely, the last declaration to be processed wins, but the order of the
+ // declarations depends on the type finder and is unspecified.
+ foreach (var composerType in types)
+ {
+ foreach (var attr in composerType.GetCustomAttributes())
+ {
+ var type = attr.EnabledType ?? composerType;
+ if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo();
+ var weight = type == composerType ? 1 : 2;
+ if (enableInfo.Weight > weight) continue;
+
+ enableInfo.Enabled = true;
+ enableInfo.Weight = weight;
+ }
+ foreach (var attr in composerType.GetCustomAttributes())
+ {
+ var type = attr.DisabledType ?? composerType;
+ if (enabled.TryGetValue(type, out var enableInfo) == false) enableInfo = enabled[type] = new EnableInfo();
+ var weight = type == composerType ? 1 : 2;
+ if (enableInfo.Weight > weight) continue;
+
+ enableInfo.Enabled = false;
+ enableInfo.Weight = weight;
+ }
+ }
+
+ // remove composers that end up being disabled
+ foreach (var kvp in enabled.Where(x => x.Value.Enabled == false))
+ types.Remove(kvp.Key);
+ }
+
+ private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements)
+ {
+ // get 'require' attributes
+ // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
+ var requireAttributes = type
+ .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces
+ .Concat(type.GetCustomAttributes()); // those marking the composer
+
+ // what happens in case of conflicting attributes (different strong/weak for same type) is not specified.
+ foreach (var attr in requireAttributes)
+ {
+ if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below)
+
+ // requiring an interface = require any enabled composer implementing that interface
+ // unless strong, and then require at least one enabled composer implementing that interface
+ if (attr.RequiredType.IsInterface)
+ {
+ var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList();
+ if (implems.Count > 0)
+ {
+ if (requirements[type] == null) requirements[type] = new List();
+ requirements[type].AddRange(implems);
+ }
+ else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak
+ throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
+ }
+ // requiring a class = require that the composer is enabled
+ // unless weak, and then requires it if it is enabled
+ else
+ {
+ if (types.Contains(attr.RequiredType))
+ {
+ if (requirements[type] == null) requirements[type] = new List();
+ requirements[type].Add(attr.RequiredType);
+ }
+ else if (attr.Weak != true) // if not explicitly set to weak, is strong
+ throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
+ }
+ }
+ }
+
+ private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection types, IDictionary> requirements)
+ {
+ // get 'required' attributes
+ // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
+ var requiredAttributes = type
+ .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces
+ .Concat(type.GetCustomAttributes()); // those marking the composer
+
+ foreach (var attr in requiredAttributes)
+ {
+ if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below)
+
+ // required by an interface = by any enabled composer implementing this that interface
+ if (attr.RequiringType.IsInterface)
+ {
+ var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList();
+ foreach (var implem in implems)
+ {
+ if (requirements[implem] == null) requirements[implem] = new List();
+ requirements[implem].Add(type);
+ }
+ }
+ // required by a class
+ else
+ {
+ if (types.Contains(attr.RequiringType))
+ {
+ if (requirements[attr.RequiringType] == null) requirements[attr.RequiringType] = new List();
+ requirements[attr.RequiringType].Add(type);
+ }
+ }
+ }
+ }
+
+ private IEnumerable InstantiateComposers(IEnumerable types)
+ {
+ IComposer InstantiateComposer(Type type)
+ {
+ var ctor = type.GetConstructor(Array.Empty());
+ if (ctor == null)
+ throw new InvalidOperationException($"Composer {type.FullName} does not have a parameter-less constructor.");
+ return (IComposer) ctor.Invoke(Array.Empty