diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
index d69d7021..00894fff 100644
--- a/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
+++ b/uSync.BackOffice/SyncHandlers/SyncHandlerContainerBase.cs
@@ -1,5 +1,11 @@
-using Microsoft.Extensions.Logging;
+using Microsoft.CodeAnalysis.Operations;
+using Microsoft.Extensions.Logging;
+using NPoco.RowMappers;
+
+using NUglify.JavaScript.Syntax;
+
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -16,6 +22,7 @@
using uSync.BackOffice.Configuration;
using uSync.BackOffice.Services;
using uSync.Core;
+using uSync.Core.Dependency;
using uSync.Core.Serialization;
namespace uSync.BackOffice.SyncHandlers
@@ -191,5 +198,81 @@ protected override bool DoItemsMatch(XElement node, TObject item)
if (base.DoItemsMatch(node, item)) return true;
return node.GetAlias().InvariantEquals(GetItemAlias(item));
}
+
+ ///
+ /// for containers, we are building a dependency graph.
+ ///
+ protected override IList GetLevelOrderedFiles(string folder, IList actions)
+ {
+ var files = syncFileService.GetFiles(folder, $"*.{this.uSyncConfig.Settings.DefaultExtension}");
+
+ var nodes = new Dictionary();
+ var graph = new List>();
+
+ foreach (var file in files)
+ {
+ var node = LoadNode(file);
+ if (node == null) continue;
+
+ var key = node.GetKey();
+ nodes.Add(key, new LeveledFile
+ {
+ Alias = node.GetAlias(),
+ File = file,
+ Level = node.GetLevel(),
+ });
+
+ // you can have circular dependencies in structure :(
+ // graph.AddRange(GetStructure(node).Select(x => GraphEdge.Create(key, x)));
+
+ graph.AddRange(GetCompositions(node).Select(x => GraphEdge.Create(key, x)));
+ }
+
+ var cleanGraph = graph.Where(x => x.Node != x.Edge).ToList();
+ var sortedList = nodes.Keys.TopologicalSort(cleanGraph);
+
+ if (sortedList == null)
+ return nodes.Values.OrderBy(x => x.Level).ToList();
+
+ var result = new List();
+ foreach(var key in sortedList)
+ {
+ if (nodes.ContainsKey(key))
+ result.Add(nodes[key]);
+ }
+ return result;
+
+ }
+
+ private IEnumerable GetStructure(XElement node)
+ {
+
+ var structure = node.Element("Structure");
+ if (structure == null) return Enumerable.Empty();
+
+ return GetKeys(structure);
+ }
+
+ private IEnumerable GetCompositions(XElement node)
+ {
+ var compositionNode = node.Element("Info")?.Element("Compositions");
+ if (compositionNode == null) return Enumerable.Empty();
+
+ return GetKeys(compositionNode);
+ }
+
+ private IEnumerable GetKeys(XElement node)
+ {
+ if (node != null)
+ {
+ foreach (var item in node.Elements())
+ {
+ var key = item.Attribute("Key").ValueOrDefault(Guid.Empty);
+ if (key == Guid.Empty) continue;
+
+ yield return key;
+ }
+ }
+ }
}
}
diff --git a/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs b/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs
index 3d274fc6..3bb89254 100644
--- a/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs
+++ b/uSync.BackOffice/SyncHandlers/SyncHandlerLevelBase.cs
@@ -130,7 +130,7 @@ protected override IEnumerable ImportFolder(string folder, HandlerS
///
/// Get all the files in a folder and return them sorted by their level
///
- private IList GetLevelOrderedFiles(string folder, IList actions)
+ protected virtual IList GetLevelOrderedFiles(string folder, IList actions)
{
List nodes = new List();
@@ -162,14 +162,14 @@ private IList GetLevelOrderedFiles(string folder, IList x.Level).ToList();
}
- private class LeveledFile
+ protected class LeveledFile
{
public string Alias { get; set; }
public int Level { get; set; }
public string File { get; set; }
}
- private XElement LoadNode(string path)
+ protected XElement LoadNode(string path)
{
syncFileService.EnsureFileExists(path);
diff --git a/uSync.Core/Dependency/DependencyGraph.cs b/uSync.Core/Dependency/DependencyGraph.cs
new file mode 100644
index 00000000..f1e25443
--- /dev/null
+++ b/uSync.Core/Dependency/DependencyGraph.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace uSync.Core.Dependency;
+
+///
+/// builds a graph of dependencies, so things can be installed in order.
+///
+public static class DependencyGraph
+{
+ public static List TopologicalSort(this ICollection nodes, ICollection> edges)
+ where T : IEquatable
+ {
+ var sortedList = new List();
+
+ // all items where they don't have a dependency on themselves.
+ var queue = new Queue(
+ nodes.Where(x => edges.All(e => e.Node.Equals(x) == false)));
+
+
+ while (queue.Any())
+ {
+ // remove this item add it to the queue.
+ var next = queue.Dequeue();
+ sortedList.Add(next);
+
+ // look for any edges for this queue.
+ foreach(var edge in edges.Where(e => e.Edge.Equals(next)).ToList())
+ {
+ var dependency = edge.Node;
+ edges.Remove(edge);
+
+ if (edges.All(x => x.Node.Equals(dependency) == false))
+ {
+ queue.Enqueue(dependency);
+ }
+ }
+ }
+
+ if (edges.Any())
+ {
+ return null;
+ }
+
+ return sortedList;
+
+ }
+}
+
+public class GraphEdge
+ where T : IEquatable
+{
+ public T Node { get; set; }
+ public T Edge { get; set; }
+
+}
+
+public class GraphEdge
+{
+ public static GraphEdge Create(T node, T edge) where T : IEquatable
+ => new GraphEdge { Node = node, Edge = edge };
+}
+
diff --git a/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs b/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs
index 0baf4e35..07bc4ab1 100644
--- a/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs
+++ b/uSync.Core/Serialization/Serializers/ContentTypeSerializer.cs
@@ -151,7 +151,7 @@ public override SyncAttempt DeserializeSecondPass(IContentType ite
SetSafeAliasValue(item, node, false);
- details.AddRange(DeserializeCompositions(item, node));
+ // details.AddRange(DeserializeCompositions(item, node));
details.AddRange(DeserializeStructure(item, node));
// When doing this reflectiony - it doesn't set is dirty.
diff --git a/uSync.Tests/Extensions/SortingTests.cs b/uSync.Tests/Extensions/SortingTests.cs
new file mode 100644
index 00000000..a8a05e3b
--- /dev/null
+++ b/uSync.Tests/Extensions/SortingTests.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+
+using NUnit.Framework;
+
+using Umbraco.Cms.Core.WebAssets;
+
+using uSync.Core.Dependency;
+
+namespace uSync.Tests.Extensions;
+
+[TestFixture]
+public class SortingTests
+{
+ private HashSet _nodes = new HashSet
+ {
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
+ };
+
+ private List _guidNodes = new List
+ {
+ Guid.Parse("{5E37F691-FF91-45DA-9F53-8641FD9FE233}"), // 0
+ Guid.Parse("{5A35701C-349C-4AAE-BBCE-964B5C196989}"), // 1
+ Guid.Parse("{C70BE6CF-4923-4E2B-8742-B90FB7BBAFCB}"), // 2
+ Guid.Parse("{DE77C37C-DBC7-4806-8B0F-332BC38A87A4}"), // 3
+ Guid.Parse("{3C90BAD8-09F2-486E-BAA7-474F0D423C4D}") // 4
+ };
+
+ [Test]
+ public void GraphSortGuidNodes()
+ {
+ var graph = new List>();
+
+ graph.Add(GraphEdge.Create(_guidNodes[2], _guidNodes[1]));
+ graph.Add(GraphEdge.Create(_guidNodes[2], _guidNodes[4]));
+
+ var result = _guidNodes.TopologicalSort(graph);
+
+ var expected = new List
+ {
+ Guid.Parse("{5E37F691-FF91-45DA-9F53-8641FD9FE233}"), // 0
+ Guid.Parse("{5A35701C-349C-4AAE-BBCE-964B5C196989}"), // 1
+ Guid.Parse("{DE77C37C-DBC7-4806-8B0F-332BC38A87A4}"), // 3
+ Guid.Parse("{3C90BAD8-09F2-486E-BAA7-474F0D423C4D}"), // 4
+ Guid.Parse("{C70BE6CF-4923-4E2B-8742-B90FB7BBAFCB}"), // 2
+ };
+
+ Assert.AreEqual(expected, result);
+ }
+
+ [Test]
+ public void GraphSortNodes()
+ {
+ var graph = new HashSet>(
+ new[]
+ {
+ GraphEdge.Create(2,4),
+ GraphEdge.Create(4,7),
+ GraphEdge.Create(5,8),
+ GraphEdge.Create(6,9),
+ });
+
+
+ var result = _nodes.TopologicalSort(graph);
+ var expected = new List {
+ 1, 3, 7, 8, 9, 10, 4, 5, 6, 2
+ };
+
+ Assert.AreEqual(expected, result);
+ }
+
+ [Test]
+ public void CircularDependencyReturnsNull()
+ {
+ var graph = new HashSet>(
+ new[]
+ {
+ GraphEdge.Create(2,4),
+ GraphEdge.Create(4,7),
+ GraphEdge.Create(5,8),
+ GraphEdge.Create(6,9),
+ GraphEdge.Create(7,2), // 7 can't depend on 2 and it depends on 4 which depends on 7
+ });
+
+ var result = _nodes.TopologicalSort(graph);
+
+ Assert.IsNull(result);
+ }
+}