Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,7 @@ The dependency filters and assertions follow the familiar filter/assert pairing:
| depends on namespace | `.WhichDependOn("x", …)` | `.DependsOn("x", …)` | `.DependOn("x", …)` |
| does not depend on | `.WhichDoNotDependOn("x", …)`| `.DoesNotDependOn("x", …)` | `.DoNotDependOn("x", …)` |
| depends only on set | `.WhichDependOnlyOn("x", …)` | `.DependsOnlyOn("x", …)`| `.DependOnlyOn("x", …)`|
| has dependencies outside set | `.WhichHaveDependenciesOutside("x", …)` | `.HasDependenciesOutside("x", …)` | `.HaveDependenciesOutside("x", …)` |

```csharp
// Presentation must not reference the data layer
Expand Down Expand Up @@ -816,22 +817,36 @@ await Expect.That(types).DoNotDependOn("MyApp.Data").ExcludingSubNamespaces();
```

For `DependsOnlyOn` a type's own namespace is always allowed, and by default so are its sub-namespaces. Use
`.ExcludingOwnSubNamespaces()` (only available on the *only-on* family) to also forbid references into a
type's own sub-namespaces:
`.ExcludingOwnSubNamespaces()` (only available on the *only-on* and *outside* families) to also forbid
references into a type's own sub-namespaces:

```csharp
await Expect.That(Types.InNamespace("MyApp.Domain"))
.DependOnlyOn("MyApp.Domain").ExcludingSubNamespaces().ExcludingOwnSubNamespaces();
```

`HasDependenciesOutside` is the **positive counterpart** of `DependsOnlyOn` for finding the violators of an
allowed set — without a double-negated "does not depend only on". The allowed set follows the same rules
(sub-namespaces included, the own namespace and framework assemblies never count as outside, the same
chainable refinements):

```csharp
// Select the current violators of an architecture rule (e.g. for a baseline)
In.AllLoadedAssemblies().Types().WhichHaveDependenciesOutside("MyApp.Application", "MyApp.Domain")

// Assert that a known legacy type still has its external dependency
await Expect.That(typeof(LegacyImportService))
.HasDependenciesOutside("MyApp.Application", "MyApp.Domain");
```

`DependsOn` and `DoesNotDependOn` (single types only) also accept a **specific type** via `<T>()` or
`(Type)`, with `.OrOn<T>()` / `.OrOn(Type)` to widen:

```csharp
await Expect.That(typeof(MyDomainType)).DoesNotDependOn<DbContext>().OrOn<SqlConnection>();
```

All three dependency families additionally accept a reusable `Filtered.Types` selection as target; see
All dependency families additionally accept a reusable `Filtered.Types` selection as target; see
[Layers as type selections](#layers-as-type-selections).

> **Framework dependencies are ignored unless you name one explicitly.** `DependOnlyOn` ignores dependencies
Expand Down Expand Up @@ -908,15 +923,16 @@ Filtered.Types repositories = Types.InNamespace("MyApp.Data").WithName("Reposi
```

The dependency assertions and filters accept such a selection as a **target**, alongside the namespace and
specific-type forms: `DependsOn` / `DoesNotDependOn` / `DependsOnlyOn` (and the plural `DependOn` /
`DoNotDependOn` / `DependOnlyOn` and the `WhichDependOn` / `WhichDoNotDependOn` / `WhichDependOnlyOn`
specific-type forms: `DependsOn` / `DoesNotDependOn` / `DependsOnlyOn` / `HasDependenciesOutside` (and the
plural `DependOn` / `DoNotDependOn` / `DependOnlyOn` / `HaveDependenciesOutside` and the `WhichDependOn` /
`WhichDoNotDependOn` / `WhichDependOnlyOn` / `WhichHaveDependenciesOutside`
filters) take one or more `Filtered.Types` arguments. Each target selection is resolved once per assertion;
a dependency matches when it is a member of the union of the resolved selections. Matching is by type
identity, where a generic type definition in the selection (e.g. a scanned `Repository<>`) matches any of
its constructions.
Multiple targets and `.OrOn(…)` mean *any of*; for the *only-on* family the union is the allowed set, while
the own-namespace and framework rules apply unchanged, including the `.ExcludingOwnSubNamespaces()` opt-out
(an empty selection thus allows only the own namespace
Multiple targets and `.OrOn(…)` mean *any of*; for the *only-on* and *outside* families the union is the
allowed set, while the own-namespace and framework rules apply unchanged, including the
`.ExcludingOwnSubNamespaces()` opt-out (an empty selection thus allows only the own namespace
and framework dependencies). A selection is an explicit target, so framework types contained in it are
matched normally by `DependsOn` / `DoesNotDependOn`.

Expand Down
122 changes: 122 additions & 0 deletions Source/aweXpect.Reflection/Collections/Filtered.Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,76 @@ public NamespaceDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces()
}
}

/// <summary>
/// A filtered collection of <see cref="System.Type" /> from a namespace-based has-dependencies-outside
/// filter, allowing to widen the allowed namespaces and to opt out of sub-namespace matching — for the
/// allowed namespaces and for the type's own namespace.
/// </summary>
/// <remarks>
/// Like all filtered collections, this is an immutable value object: <see cref="OrOn" />,
/// <see cref="ExcludingSubNamespaces" /> and <see cref="ExcludingOwnSubNamespaces" /> do not mutate this
/// instance but rebuild a fresh filter from the original base collection, so deriving multiple views from
/// the same instance cannot corrupt each other.
/// </remarks>
public sealed class NamespaceDependencyOutsideFilterResult : Types
{
private readonly Func<NamespaceDependencyOptions, Types> _build;
private readonly NamespaceDependencyOptions _options;

internal NamespaceDependencyOutsideFilterResult(
NamespaceDependencyOptions options,
Func<NamespaceDependencyOptions, Types> build)
: base(build(options))
{
_options = options;
_build = build;
}

/// <summary>
/// Widens the allowed set by the given <paramref name="namespaces" /> (including sub-namespaces unless
/// <see cref="ExcludingSubNamespaces" /> is used), so that dependencies on them no longer count as
/// outside.
/// </summary>
public NamespaceDependencyOutsideFilterResult OrOn(params IEnumerable<string> namespaces)
{
NamespaceDependencyOptions widened = _options.Copy();
widened.OrOn(namespaces);
return new NamespaceDependencyOutsideFilterResult(widened, _build);
}

/// <summary>
/// Excludes sub-namespaces of the allowed namespaces from matching for the whole filter (including any
/// <see cref="OrOn" /> additions), so that dependencies on them count as outside.
/// </summary>
/// <remarks>
/// Without this call, a namespace matches itself and all its sub-namespaces (so <c>Foo.Bar</c> includes
/// <c>Foo.Bar.Baz</c> but not <c>Foo.BarBaz</c>).
/// <para />
/// The type's own namespace never counts as outside, and neither do its sub-namespaces unless
/// <see cref="ExcludingOwnSubNamespaces" /> is also used.
/// </remarks>
public NamespaceDependencyOutsideFilterResult ExcludingSubNamespaces()
{
NamespaceDependencyOptions refined = _options.Copy();
refined.ExcludingSubNamespaces();
return new NamespaceDependencyOutsideFilterResult(refined, _build);
}

/// <summary>
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a <c>Foo</c>
/// type referencing <c>Foo.Bar</c> is selected unless <c>Foo.Bar</c> is explicitly allowed).
/// </summary>
/// <remarks>
/// The type's own namespace itself never counts as outside.
/// </remarks>
public NamespaceDependencyOutsideFilterResult ExcludingOwnSubNamespaces()
{
NamespaceDependencyOptions refined = _options.Copy();
refined.ExcludingOwnSubNamespaces();
return new NamespaceDependencyOutsideFilterResult(refined, _build);
}
}

/// <summary>
/// A filtered collection of <see cref="System.Type" /> within a namespace, that also allows clarifying
/// the assembly source once (it defaults to all loaded assemblies).
Expand Down Expand Up @@ -628,5 +698,57 @@ public TypeSetDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces()
return new TypeSetDependencyOnlyOnFilterResult(refined, _build);
}
}

/// <summary>
/// A filtered collection of <see cref="System.Type" /> from a has-dependencies-outside filter whose
/// allowed targets are filtered collections of types, allowing to widen the allowed collections and to
/// opt out of the implicit allowance of the type's own sub-namespaces.
/// </summary>
/// <remarks>
/// Like all filtered collections, this is an immutable value object: <see cref="OrOn" /> and
/// <see cref="ExcludingOwnSubNamespaces" /> do not mutate this instance but rebuild a fresh filter from
/// the original base collection, so deriving multiple views from the same instance cannot corrupt each
/// other.
/// </remarks>
public sealed class TypeSetDependencyOutsideFilterResult : Types
{
private readonly Func<TypeSetDependencyOptions, Types> _build;
private readonly TypeSetDependencyOptions _options;

internal TypeSetDependencyOutsideFilterResult(
TypeSetDependencyOptions options,
Func<TypeSetDependencyOptions, Types> build)
: base(build(options))
{
_options = options;
_build = build;
}

/// <summary>
/// Widens the allowed set by the given <paramref name="targets" />, so that dependencies on their
/// types no longer count as outside.
/// </summary>
public TypeSetDependencyOutsideFilterResult OrOn(params Filtered.Types[] targets)
{
TypeSetDependencyOptions widened = _options.Copy();
widened.OrOn(targets);
return new TypeSetDependencyOutsideFilterResult(widened, _build);
}

/// <summary>
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a <c>Foo</c>
/// type referencing <c>Foo.Bar</c> is selected unless <c>Foo.Bar</c> types are part of an allowed
/// collection).
/// </summary>
/// <remarks>
/// The type's own namespace itself never counts as outside.
/// </remarks>
public TypeSetDependencyOutsideFilterResult ExcludingOwnSubNamespaces()
{
TypeSetDependencyOptions refined = _options.Copy();
refined.ExcludingOwnSubNamespaces();
return new TypeSetDependencyOutsideFilterResult(refined, _build);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using aweXpect.Customization;
using aweXpect.Reflection.Collections;
using aweXpect.Reflection.Helpers;
using aweXpect.Reflection.Options;

namespace aweXpect.Reflection;

public static partial class TypeFilters
{
/// <summary>
/// Filter for types which have at least one dependency (a type referenced in their signature) outside the
/// allowed <paramref name="namespaces" /> (including sub-namespaces), their own namespace and framework
/// assemblies — the positive counterpart of
/// <see cref="WhichDependOnlyOn(Filtered.Types, IEnumerable{string})" /> for finding violators of an
/// allowed set.
/// </summary>
/// <remarks>
/// Dependencies on types whose assembly name matches one of the
/// <see cref="AwexpectCustomization.ReflectionCustomizationValue.ExcludedAssemblyPrefixes" /> at a
/// name-segment boundary (<c>System</c> covers <c>System.Text.Json</c>, but not
/// <c>SystemsBiology.Core</c>) are ignored, so that framework dependencies never count as outside the
/// allowed set. The default prefixes include <c>Microsoft</c>, so e.g. a dependency on
/// <c>Microsoft.EntityFrameworkCore</c> is also ignored; customize the prefixes to make such a dependency
/// count.
/// </remarks>
public static Filtered.Types.NamespaceDependencyOutsideFilterResult WhichHaveDependenciesOutside(
this Filtered.Types @this, params IEnumerable<string> namespaces)
=> new(new NamespaceDependencyOptions(namespaces),
options => @this.Which(Filter.Suffix<Type>(
type => type.HasDependencyNamespaceViolations(options),
() => $"which have dependencies outside {options.Describe()} ")));

/// <summary>
/// Filter for types which have at least one dependency (a type referenced in their signature) outside the
/// allowed set formed by the filtered collections of types <paramref name="target" /> and
/// <paramref name="additional" />, their own namespace and framework assemblies — the positive counterpart
/// of <see cref="WhichDependOnlyOn(Filtered.Types, Filtered.Types, Filtered.Types[])" /> for finding
/// violators of an allowed set.
/// </summary>
/// <remarks>
/// The target collections are resolved once per filter; a dependency is inside the allowed set when it is
/// a member of the union of the resolved collections (by <see cref="Type" /> identity; a generic type
/// definition in a collection matches any construction of it). A type's own namespace never counts as
/// outside, including its sub-namespaces unless
/// <see cref="Filtered.Types.TypeSetDependencyOutsideFilterResult.ExcludingOwnSubNamespaces" /> is used.
/// <para />
/// Dependencies on types whose assembly name matches one of the
/// <see cref="AwexpectCustomization.ReflectionCustomizationValue.ExcludedAssemblyPrefixes" /> at a
/// name-segment boundary (<c>System</c> covers <c>System.Text.Json</c>, but not
/// <c>SystemsBiology.Core</c>) are ignored, so that framework dependencies never count as outside the
/// allowed set. The default prefixes include <c>Microsoft</c>, so e.g. a dependency on
/// <c>Microsoft.EntityFrameworkCore</c> is also ignored; customize the prefixes to make such a dependency
/// count.
/// </remarks>
public static Filtered.Types.TypeSetDependencyOutsideFilterResult WhichHaveDependenciesOutside(
this Filtered.Types @this, Filtered.Types target, params Filtered.Types[] additional)
=> new(new TypeSetDependencyOptions(target, additional),
options => @this.Which(Filter.Suffix<Type>(
async type =>
{
ResolvedTypeSet allowed = await options.Resolve();
return type.HasDependencyTypeSetViolations(allowed);
},
// The parentheses delimit the target description (which ends in the target's source scope,
// e.g. "in all loaded assemblies") from the subject collection's own source suffix.
() => $"which have dependencies outside ({options.Describe()}) ")));
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace aweXpect.Reflection.Helpers;

/// <summary>
/// Renders the grouped failure output of the depend-only-on constraints: one indented line per failing
/// item, each followed by its list of disallowed dependencies.
/// Renders the grouped failure output of the depend-only-on and has-dependencies-outside constraints: one
/// indented line per failing item, each followed by its list of dependencies outside the allowed set.
/// </summary>
/// <remarks>
/// Shared between the assembly-level and the type-level constraints, so that the formatting (indentation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ public NamespaceDependencyOptions Copy()
public bool ExcludeSubNamespaces => _excludeSubNamespaces;

/// <summary>
/// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on).
/// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on
/// and has-dependencies-outside).
/// </summary>
/// <remarks>
/// The type's own namespace is always allowed; its sub-namespaces stay allowed unless the caller opted into
Expand Down Expand Up @@ -123,7 +124,8 @@ public void ExcludingSubNamespaces()
=> _excludeSubNamespaces = true;

/// <summary>
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on).
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on
/// and has-dependencies-outside).
/// </summary>
public void ExcludingOwnSubNamespaces()
=> _excludeOwnSubNamespaces = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ private TypeSetDependencyOptions(IEnumerable<Filtered.Types> targets, bool exclu
}

/// <summary>
/// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on).
/// Indicates whether sub-namespaces of the type's own namespace are still allowed (for depends-only-on
/// and has-dependencies-outside).
/// </summary>
/// <remarks>
/// The type's own namespace is always allowed; its sub-namespaces stay allowed unless the caller opted into
Expand Down Expand Up @@ -79,7 +80,8 @@ public void OrOn(Filtered.Types[] targets)
}

/// <summary>
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on).
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (for depends-only-on
/// and has-dependencies-outside).
/// </summary>
/// <remarks>
/// Only the exemption rule changes, not the resolved target set, so a previously resolved set stays valid.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System.Collections.Generic;
using aweXpect.Core;
using aweXpect.Reflection.Options;
using aweXpect.Results;

namespace aweXpect.Reflection.Results;

/// <summary>
/// The result of a namespace-based has-dependencies-outside assertion, allowing to widen the allowed
/// namespaces and to opt out of sub-namespace matching — for the allowed namespaces and for the type's own
/// namespace.
/// </summary>
public sealed class NamespaceDependencyOutsideResult<TThat>
: AndOrResult<TThat, IThat<TThat>>
{
private readonly NamespaceDependencyOptions _options;

internal NamespaceDependencyOutsideResult(
ExpectationBuilder expectationBuilder,
IThat<TThat> subject,
NamespaceDependencyOptions options)
: base(expectationBuilder, subject)
=> _options = options;

/// <summary>
/// Widens the allowed set by the given <paramref name="namespaces" /> (including sub-namespaces unless
/// <see cref="ExcludingSubNamespaces" /> is used), so that dependencies on them no longer count as outside.
/// </summary>
public NamespaceDependencyOutsideResult<TThat> OrOn(params IEnumerable<string> namespaces)
{
_options.OrOn(namespaces);
return this;
}

/// <summary>
/// Excludes sub-namespaces of the allowed namespaces from matching for the whole expression (including any
/// <see cref="OrOn" /> additions), so that dependencies on them count as outside.
/// </summary>
/// <remarks>
/// Without this call, a namespace matches itself and all its sub-namespaces (so <c>Foo.Bar</c> includes
/// <c>Foo.Bar.Baz</c> but not <c>Foo.BarBaz</c>).
/// <para />
/// The type's own namespace never counts as outside, and neither do its sub-namespaces unless
/// <see cref="ExcludingOwnSubNamespaces" /> is also used.
/// </remarks>
public NamespaceDependencyOutsideResult<TThat> ExcludingSubNamespaces()
{
_options.ExcludingSubNamespaces();
return this;
}

/// <summary>
/// Excludes sub-namespaces of the type's own namespace from being implicitly allowed (so a <c>Foo</c> type
/// referencing <c>Foo.Bar</c> has a dependency outside the allowed set unless <c>Foo.Bar</c> is explicitly
/// allowed).
/// </summary>
/// <remarks>
/// The type's own namespace itself never counts as outside.
/// </remarks>
public NamespaceDependencyOutsideResult<TThat> ExcludingOwnSubNamespaces()
{
_options.ExcludingOwnSubNamespaces();
return this;
}
}
Loading
Loading