diff --git a/src/Umbraco.Core/Composing/Composers.cs b/src/Umbraco.Core/Composing/Composers.cs index 14cb0dce8e..0510740e42 100644 --- a/src/Umbraco.Core/Composing/Composers.cs +++ b/src/Umbraco.Core/Composing/Composers.cs @@ -70,7 +70,23 @@ namespace Umbraco.Core.Composing } } - private IEnumerable PrepareComposerTypes() + internal IEnumerable PrepareComposerTypes() + { + var requirements = GetRequirements(); + + // only for debugging, this is verbose + //_logger.Debug(GetComposersReport(requirements)); + + var sortedComposerTypes = SortComposers(requirements); + + // 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; + } + + internal Dictionary> GetRequirements(bool throwOnMissing = true) { // create a list, remove those that cannot be enabled due to runtime level var composerTypeList = _composerTypes @@ -89,25 +105,69 @@ namespace Umbraco.Core.Composing // 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) + void GatherInterfaces(Type type, Func getTypeInAttribute, HashSet iset, List set2) + where TAttribute : Attribute { - GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements); - GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements); + foreach (var attribute in type.GetCustomAttributes()) + { + var typeInAttribute = getTypeInAttribute(attribute); + if (typeInAttribute != null && // if the attribute references a type ... + typeInAttribute.IsInterface && // ... which is an interface ... + typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ... + !iset.Contains(typeInAttribute)) // ... which is not already in the list + { + // add it to the new list + iset.Add(typeInAttribute); + set2.Add(typeInAttribute); + + // add all its interfaces implementing IComposer + foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x))) + { + iset.Add(i); + set2.Add(i); + } + } + } } - // only for debugging, this is verbose - //_logger.Debug(GetComposersReport(requirements)); + // gather interfaces too + var interfaces = new HashSet(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y)))); + composerTypeList.AddRange(interfaces); + var list1 = composerTypeList; + while (list1.Count > 0) + { + var list2 = new List(); + foreach (var t in list1) + { + GatherInterfaces(t, a => a.RequiredType, interfaces, list2); + GatherInterfaces(t, a => a.RequiringType, interfaces, list2); + } + composerTypeList.AddRange(list2); + list1 = list2; + } + // sort the composers according to their dependencies + var requirements = new Dictionary>(); + foreach (var type in composerTypeList) + requirements[type] = null; + foreach (var type in composerTypeList) + { + GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing); + GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements); + } + + return requirements; + } + + internal IEnumerable SortComposers(Dictionary> 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(); + sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList(); } catch (Exception e) { @@ -117,40 +177,37 @@ namespace Umbraco.Core.Composing 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) + internal static string GetComposersReport(Dictionary> requirements) { var text = new StringBuilder(); text.AppendLine("Composers & Dependencies:"); + text.AppendLine(" < compose before"); + text.AppendLine(" > compose after"); + text.AppendLine(" : implements"); + text.AppendLine(" = depends"); text.AppendLine(); + bool HasReq(IEnumerable types, Type type) + => types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface); + 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); + var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true); + text.AppendLine(" > " + attribute.RequiredType + + (weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")"); } + foreach (var attribute in type.GetCustomAttributes()) + text.AppendLine(" < " + attribute.RequiringType); + foreach (var i in type.GetInterfaces()) + text.AppendLine(" : " + i.FullName); if (kvp.Value != null) foreach (var t in kvp.Value) text.AppendLine(" = " + t); @@ -221,16 +278,16 @@ namespace Umbraco.Core.Composing types.Remove(kvp.Key); } - private static void GatherRequirementsFromRequireAttribute(Type type, ICollection types, IDictionary> requirements) + private static void GatherRequirementsFromAfterAttribute(Type type, ICollection types, IDictionary> requirements, bool throwOnMissing = true) { // get 'require' attributes // these attributes are *not* inherited because we want to "custom-inherit" for interfaces only - var requireAttributes = type + var afterAttributes = 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) + foreach (var attr in afterAttributes) { if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below) @@ -238,13 +295,13 @@ namespace Umbraco.Core.Composing // 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(); + var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).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 + else if (attr.Weak == false && throwOnMissing) // 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 @@ -256,28 +313,28 @@ namespace Umbraco.Core.Composing 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 + else if (attr.Weak != true && throwOnMissing) // 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) + private static void GatherRequirementsFromBeforeAttribute(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 + var beforeAttributes = type .GetInterfaces().SelectMany(x => x.GetCustomAttributes()) // those marking interfaces .Concat(type.GetCustomAttributes()); // those marking the composer - foreach (var attr in requiredAttributes) + foreach (var attr in beforeAttributes) { 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(); + var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList(); foreach (var implem in implems) { if (requirements[implem] == null) requirements[implem] = new List(); diff --git a/src/Umbraco.Tests/Components/ComponentTests.cs b/src/Umbraco.Tests/Components/ComponentTests.cs index c026e5a157..2ba94d8c78 100644 --- a/src/Umbraco.Tests/Components/ComponentTests.cs +++ b/src/Umbraco.Tests/Components/ComponentTests.cs @@ -4,6 +4,7 @@ using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Cache; using Umbraco.Core.Compose; using Umbraco.Core.Composing; using Umbraco.Core.IO; @@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer2) }; composers = new Composers(composition, types, Mock.Of()); Composed.Clear(); Assert.Throws(() => composers.Compose()); + Console.WriteLine("throws:"); + composers = new Composers(composition, types, Mock.Of()); + requirements = composers.GetRequirements(false); + Console.WriteLine(Composers.GetComposersReport(requirements)); types = new[] { typeof(Composer12) }; composers = new Composers(composition, types, Mock.Of()); @@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components Assert.AreEqual(typeof(Composer27), Composed[1]); } + [Test] + public void AllComposers() + { + var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of()); + + var register = MockRegister(); + var composition = new Composition(register, typeLoader, Mock.Of(), MockRuntimeState(RuntimeLevel.Run)); + + var types = typeLoader.GetTypes().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web")); + var composers = new Composers(composition, types, Mock.Of()); + var requirements = composers.GetRequirements(); + var report = Composers.GetComposersReport(requirements); + Console.WriteLine(report); + var composerTypes = composers.SortComposers(requirements); + + foreach (var type in composerTypes) + Console.WriteLine(type); + } + #region Compothings public class TestComposerBase : IComposer diff --git a/src/Umbraco.Web/Runtime/WebFinalComposer.cs b/src/Umbraco.Web/Runtime/WebFinalComposer.cs index 64d7725848..c69ae1af1a 100644 --- a/src/Umbraco.Web/Runtime/WebFinalComposer.cs +++ b/src/Umbraco.Web/Runtime/WebFinalComposer.cs @@ -3,7 +3,9 @@ namespace Umbraco.Web.Runtime { // web's final composer composes after all user composers + // and *also* after ICoreComposer (in case IUserComposer is disabled) [ComposeAfter(typeof(IUserComposer))] + [ComposeAfter(typeof(ICoreComposer))] public class WebFinalComposer : ComponentComposer { } }