diff --git a/ArchUnitNET/Domain/Namespace.cs b/ArchUnitNET/Domain/Namespace.cs
index db8afdd44..b958c1a07 100644
--- a/ArchUnitNET/Domain/Namespace.cs
+++ b/ArchUnitNET/Domain/Namespace.cs
@@ -1,13 +1,83 @@
using System.Collections.Generic;
+using System.Linq;
+using ArchUnitNET.Domain.Dependencies;
namespace ArchUnitNET.Domain
{
- public class Namespace : Slice, IHasName
+ ///
+ /// A namespace of the loaded architecture together with the types declared directly in it.
+ /// Namespaces are equal exactly when their full names are.
+ ///
+ public class Namespace : IHasName, IHasDescription, IHasDependencies
{
+ private readonly string _name;
+ private readonly List _types;
+
public Namespace(string name, IEnumerable types)
- : base(SliceIdentifier.Of(name), types) { }
+ {
+ _name = name;
+ _types = types.ToList();
+ }
+
+ /// The full name of this namespace.
+ public string Name => _name;
+
+ /// The full name of this namespace.
+ public string FullName => _name;
+
+ /// The full name of this namespace.
+ public string Description => _name;
+
+ /// The types declared directly in this namespace.
+ public IEnumerable Types => _types;
+
+ /// The classes declared directly in this namespace.
+ public IEnumerable Classes => Types.OfType();
+
+ /// The interfaces declared directly in this namespace.
+ public IEnumerable Interfaces => Types.OfType();
+
+ /// Every dependency going out of this namespace's types, including internal ones.
+ public List Dependencies =>
+ Types.SelectMany(type => type.Dependencies).ToList();
+
+ /// Every dependency pointing at this namespace's types.
+ public List BackwardsDependencies =>
+ Types.SelectMany(type => type.BackwardsDependencies).ToList();
+
+ internal void AddType(IType type)
+ {
+ _types.Add(type);
+ }
+
+ private bool Equals(Namespace other)
+ {
+ return FullName == other.FullName;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj))
+ {
+ return false;
+ }
+
+ if (ReferenceEquals(this, obj))
+ {
+ return true;
+ }
+
+ return obj.GetType() == GetType() && Equals((Namespace)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return FullName != null ? FullName.GetHashCode() : 0;
+ }
- public string Name => Description;
- public string FullName => Description;
+ public override string ToString()
+ {
+ return FullName;
+ }
}
}
diff --git a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
index b02092c59..7432a2354 100644
--- a/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
+++ b/ArchUnitNET/Domain/PlantUml/Export/PlantUmlFileBuilder.cs
@@ -139,13 +139,15 @@ public PlantUmlFileBuilder WithDependenciesFrom(
foreach (var slice in _sliceList)
{
var sliceIsPackage = IsPackage(slice);
- var dependencyTargets = SelectDependencies(slice, generationOptions);
+ var dependencyTargets = SelectDependencies(
+ slice,
+ _sliceList,
+ target => target.Description,
+ target => target.Types,
+ generationOptions
+ );
- if (slice is Namespace) //This throws errors if namespaces and slices are used in the same diagram, the syntax can't be mixed in PlantUML
- {
- nodes.Add(slice, new PlantUmlNamespace(slice.Description));
- }
- else if (!sliceIsPackage)
+ if (!sliceIsPackage)
{
var slc = new PlantUmlSlice(slice.Description, slice.NameSpace);
if (generationOptions.C4Style)
@@ -222,11 +224,9 @@ public PlantUmlFileBuilder WithDependenciesFrom(
if (!generationOptions.IncludeNodesWithoutDependencies)
{
- nodes = RemoveNodesNotContainedInDependencies(nodes);
+ RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description);
}
- var nodeElements = HandleNodes(nodes);
-
if (!generationOptions.LimitDependencies)
{
RemoveDuplicateDependenciesWhenShowingPackages();
@@ -234,10 +234,94 @@ public PlantUmlFileBuilder WithDependenciesFrom(
}
RemoveDuplicatedArrowsIfExist();
- _diagram.AddElements(
- nodeElements.OrderBy(element =>
- element is PlantUmlNamespace @namespace ? @namespace.Name.Length : -1
+ _diagram.AddElements(nodes.Values);
+ _diagram.AddElements(_dependencies);
+
+ return this;
+ }
+
+ ///
+ /// Draws the given namespaces as nested PlantUML namespaces, with an arrow from one
+ /// namespace to another wherever one of its types depends on one of the other's. Parent
+ /// namespaces that are not in the list are added so that the nesting is complete.
+ ///
+ /// The namespaces to draw, e.g. .
+ /// Filters and layout options; the defaults if null.
+ /// does not apply to namespaces.
+ public PlantUmlFileBuilder WithDependenciesFrom(
+ IEnumerable namespaces,
+ GenerationOptions generationOptions = null
+ )
+ {
+ var namespaceList = namespaces.Distinct().ToList();
+ if (generationOptions == null)
+ {
+ generationOptions = new GenerationOptions();
+ }
+
+ var dependencyType = generationOptions.LimitDependencies
+ ? DependencyType.OneToOneCompact
+ : DependencyType.OneToOne;
+ var nodes = new Dictionary();
+
+ foreach (var ns in namespaceList)
+ {
+ nodes.Add(ns, new PlantUmlNamespace(ns.FullName));
+ _dependencies.AddRange(
+ SelectDependencies(
+ ns,
+ namespaceList,
+ target => target.FullName,
+ target => target.Types,
+ generationOptions
+ )
+ .Select(target => new PlantUmlDependency(
+ ns.FullName,
+ target.FullName,
+ dependencyType
+ ))
+ );
+ }
+
+ if (!generationOptions.IncludeNodesWithoutDependencies)
+ {
+ RemoveNodesNotContainedInDependencies(nodes, ns => ns.FullName);
+ }
+
+ var nodeElements = nodes.Values.ToList();
+ foreach (var ns in nodes.Keys)
+ {
+ var namespaceName = ns.FullName;
+ var dotIndex = namespaceName.Length;
+ var parentNamespaces = new List();
+ while (dotIndex != -1)
+ {
+ namespaceName = namespaceName.Substring(0, dotIndex);
+ parentNamespaces.Add(namespaceName);
+ dotIndex = namespaceName.LastIndexOf('.');
+ }
+
+ parentNamespaces.Reverse();
+ foreach (
+ var parent in parentNamespaces.Where(parentName =>
+ nodeElements
+ .OfType()
+ .All(element => element.Name != parentName)
+ )
)
+ {
+ nodeElements.Add(new PlantUmlNamespace(parent));
+ }
+ }
+
+ if (!generationOptions.LimitDependencies)
+ {
+ ReplaceCirclesWithAppropriateDependencyType();
+ }
+ RemoveDuplicatedArrowsIfExist();
+
+ _diagram.AddElements(
+ nodeElements.OrderBy(element => ((PlantUmlNamespace)element).Name.Length)
);
_diagram.AddElements(_dependencies);
@@ -282,7 +366,13 @@ string package
foreach (var slice in _sliceList)
{
var sliceIsPackage = IsPackage(slice);
- var dependencyTargets = SelectDependencies(slice, generationOptions);
+ var dependencyTargets = SelectDependencies(
+ slice,
+ _sliceList,
+ target => target.Description,
+ target => target.Types,
+ generationOptions
+ );
if (sliceIsPackage)
{
@@ -305,11 +395,7 @@ string package
);
}
- if (slice is Namespace) //This throws errors if namespaces and slices are used in the same diagram, the syntax can't be mixed in PlantUML
- {
- nodes.Add(slice, new PlantUmlNamespace(slice.Description));
- }
- else if (!sliceIsPackage)
+ if (!sliceIsPackage)
{
nodes.Add(
slice,
@@ -326,7 +412,7 @@ string package
if (!generationOptions.IncludeNodesWithoutDependencies)
{
- nodes = RemoveNodesNotContainedInDependencies(nodes);
+ RemoveNodesNotContainedInDependencies(nodes, slice => slice.Description);
}
RemoveDuplicateDependenciesWhenShowingPackages();
@@ -356,13 +442,7 @@ string package
}
}
- var nodeElements = HandleNodes(nodes);
-
- _diagram.AddElements(
- nodeElements.OrderBy(element =>
- element is PlantUmlNamespace @namespace ? @namespace.Name.Length : -1
- )
- );
+ _diagram.AddElements(nodes.Values);
var tempList = _sliceList
.Where(slice => IsPackage(slice) && slice.Description.Contains(package))
@@ -381,72 +461,52 @@ string package
return this;
}
- private Dictionary RemoveNodesNotContainedInDependencies(
- Dictionary nodes
+ ///
+ /// Removes the nodes whose name no dependency starts or ends at.
+ ///
+ private void RemoveNodesNotContainedInDependencies(
+ Dictionary nodes,
+ Func nameOf
)
{
+ var namesInDependencies = new HashSet(
+ _dependencies.SelectMany(dep => new[] { dep.Origin, dep.Target })
+ );
+ // Collected first: removing entries while enumerating the dictionary throws on
+ // .NET Framework.
foreach (
- var entry in nodes.Where(node =>
- !_dependencies
- .SelectMany(dep => new[] { dep.Origin, dep.Target })
- .Contains(node.Key.Description)
- )
+ var key in nodes
+ .Keys.Where(key => !namesInDependencies.Contains(nameOf(key)))
+ .ToList()
)
{
- nodes.Remove(entry.Key);
+ nodes.Remove(key);
}
- return nodes;
}
- private static IEnumerable HandleNodes(
- Dictionary nodes
+ ///
+ /// The candidates, other than itself, that one of the origin's
+ /// dependencies passing points into.
+ ///
+ private static IEnumerable SelectDependencies(
+ T origin,
+ IEnumerable candidates,
+ Func nameOf,
+ Func> typesOf,
+ GenerationOptions generationOptions
)
+ where T : IHasDependencies
{
- var nodeElements = nodes.Values.ToList();
-
- foreach (var node in nodes)
- {
- if (node.Key is Namespace)
- {
- var namespaceName = node.Key.Description;
- var dotIndex = namespaceName.Length;
- var parentNamespaces = new List();
- while (dotIndex != -1)
- {
- namespaceName = namespaceName.Substring(0, dotIndex);
- parentNamespaces.Add(namespaceName);
- dotIndex = namespaceName.LastIndexOf('.');
- }
-
- parentNamespaces.Reverse();
- foreach (
- var namespc in parentNamespaces.Where(namespcName =>
- nodeElements
- .OfType()
- .All(element => element.Name != namespcName)
- )
- )
- {
- nodeElements.Add(new PlantUmlNamespace(namespc));
- }
- }
- }
-
- return nodeElements;
- }
-
- private IEnumerable SelectDependencies(
- Slice slice,
- GenerationOptions generationOptions
- ) =>
- _sliceList.Where(targetSlice =>
- targetSlice.Description != slice.Description
- && slice
+ var originName = nameOf(origin);
+ return candidates.Where(target =>
+ nameOf(target) != originName
+ && origin
.Dependencies.Where(dep =>
generationOptions.DependencyFilter?.Invoke(dep) ?? true
)
- .Any(dep => targetSlice.Types.Contains(dep.Target))
+ .Any(dep => typesOf(target).Contains(dep.Target))
);
+ }
private void RemovePatternInappropriateSlices(string thatContainsThisString = null)
{
diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs
index dceed9363..27c49304e 100644
--- a/ArchUnitNET/Loader/TypeProcessor.cs
+++ b/ArchUnitNET/Loader/TypeProcessor.cs
@@ -1119,7 +1119,7 @@ internal static void AddTypesToNamespaces(IEnumerable types)
{
foreach (var type in types)
{
- ((List)type.Namespace.Types).Add(type);
+ type.Namespace.AddType(type);
}
}
}
diff --git a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs
index 601349c51..64302e59f 100644
--- a/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs
+++ b/ArchUnitNETTests/Domain/PlantUml/PlantUmlFileBuilderTest.cs
@@ -55,6 +55,16 @@ public void BuildUmlByNamespacesTest()
Assert.NotEmpty(uml);
}
+ [Fact]
+ public Task BuildUmlByNamespacesSnapshotTest()
+ {
+ var namespaces = StaticTestArchitectures.SlicesTestArchitecture.Namespaces.Where(ns =>
+ ns.FullName.StartsWith("SlicesTestAssembly.MultipleSubnamespaces")
+ );
+ var uml = new PlantUmlFileBuilder().WithDependenciesFrom(namespaces).AsString();
+ return Verifier.Verify(uml).DisableDiff().UseDirectory("Snapshots");
+ }
+
[Fact]
public void BuildUmlBySlicesTest()
{
diff --git a/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesSnapshotTest.verified.txt b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesSnapshotTest.verified.txt
new file mode 100644
index 000000000..5b87b1575
--- /dev/null
+++ b/ArchUnitNETTests/Domain/PlantUml/Snapshots/PlantUmlFileBuilderTest.BuildUmlByNamespacesSnapshotTest.verified.txt
@@ -0,0 +1,35 @@
+@startuml
+
+!include https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml
+
+HIDE_STEREOTYPE()
+
+namespace SlicesTestAssembly {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3 {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2 {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1 {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2 {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1 {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group2.Inner {
+}
+namespace SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1.Inner {
+}
+[SlicesTestAssembly.MultipleSubnamespaces.Slice3] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice1]
+[SlicesTestAssembly.MultipleSubnamespaces.Slice3.Group1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3]
+[SlicesTestAssembly.MultipleSubnamespaces.Slice2] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3]
+[SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice3]
+[SlicesTestAssembly.MultipleSubnamespaces.Slice1] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2]
+[SlicesTestAssembly.MultipleSubnamespaces.Slice1.Service] --|> [SlicesTestAssembly.MultipleSubnamespaces.Slice2.Service]
+@enduml
diff --git a/ArchUnitNETTests/Loader/NamespaceTests.cs b/ArchUnitNETTests/Loader/NamespaceTests.cs
index a5c56c426..4d29d6bfd 100644
--- a/ArchUnitNETTests/Loader/NamespaceTests.cs
+++ b/ArchUnitNETTests/Loader/NamespaceTests.cs
@@ -1,4 +1,6 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
+using ArchUnitNET.Domain;
using Xunit;
namespace ArchUnitNETTests.Loader
@@ -25,5 +27,21 @@ public void NoEmptyNamespacesTest()
)
);
}
+
+ [Fact]
+ public void NamespacesAreEqualByName()
+ {
+ var ns = new Namespace("A.B", new List());
+ var sameName = new Namespace("A.B", new List());
+ var otherName = new Namespace("A.C", new List());
+
+ Assert.Equal(ns, sameName);
+ Assert.Equal(ns.GetHashCode(), sameName.GetHashCode());
+ Assert.NotEqual(ns, otherName);
+ Assert.Equal("A.B", ns.Name);
+ Assert.Equal("A.B", ns.FullName);
+ Assert.Equal("A.B", ns.Description);
+ Assert.Equal("A.B", ns.ToString());
+ }
}
}