Skip to content

Commit cb41239

Browse files
authored
feat!: add Types static entry point and remove In.Namespace (#327)
`In.Namespace(...)` read poorly in architecture rules and hid that it selects types. The new `Types` class is the entry point for selecting types by criteria, while `In` keeps starting from concrete reflection objects: - `Types.InNamespace(ns)` replaces `In.Namespace(ns)` and returns `Filtered.Types.InNamespaceResult`, which defaults to all loaded assemblies and allows clarifying the assembly source once (before any further filters) via the same `In*` methods. - `Types.InAllLoadedAssemblies()`, `Types.InAssemblies(...)`, `Types.InAssemblyContaining<T>()` / `(Type)`, `Types.InEntryAssembly()` and `Types.InExecutingAssembly()` select all types in the respective assemblies.
1 parent bb37d17 commit cb41239

15 files changed

Lines changed: 458 additions & 111 deletions

README.md

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
Expectations for reflection types for [aweXpect](https://github.com/Testably/aweXpect).
1010

1111
Write architecture and convention tests as plain, readable assertions: **select** the assemblies, types
12-
or members you care about with `In`, then **assert** a rule on them with `Expect.That`.
12+
or members you care about with `In` or `Types`, then **assert** a rule on them with `Expect.That`.
1313

1414
## At a glance
1515

@@ -98,9 +98,33 @@ evaluated collection that you can navigate and filter further.
9898
| `In.ExecutingAssembly()` | the executing assembly |
9999
| `In.Type<T>()` / `In.Type(typeof(T))` | a single type |
100100
| `In.Types<T1, T2>()` / `In.Types<T1, T2, T3>()` / `In.Types(t1, t2, …)` | the given types |
101-
| `In.Namespace("ns")` | all types within a namespace and its sub-namespaces (across loaded assemblies) |
102101
| `In.Constructors(…)` / `In.Events(…)` / `In.Fields(…)` / `In.Methods(…)` / `In.Properties(…)` | the given members |
103102

103+
## Sources: the `Types` helper
104+
105+
While `In` starts from concrete reflection objects, `Types` selects types *by criteria* — the natural entry
106+
point for architecture rules:
107+
108+
| Source | Returns |
109+
|-----------------------------------------------------------------|---------------------------------------------------------------------------------|
110+
| `Types.InNamespace("ns")` | all types within a namespace and its sub-namespaces (across loaded assemblies) |
111+
| `Types.InAllLoadedAssemblies()` | all types in all currently loaded assemblies |
112+
| `Types.InAssemblies(a1, a2, …)` | all types in the given assemblies |
113+
| `Types.InAssemblyContaining<T>()` / `…(typeof(T))` | all types in the assembly that declares `T` |
114+
| `Types.InEntryAssembly()` / `Types.InExecutingAssembly()` | all types in the entry / executing assembly |
115+
116+
`Types.InNamespace(…)` searches all loaded assemblies by default; chain one of the same `In*` methods directly
117+
after it to clarify the assembly source (it can only be specified once, before any further filters):
118+
119+
```csharp
120+
// Defaults to all loaded assemblies
121+
await Expect.That(Types.InNamespace("MyApp.Domain")).AreSealed();
122+
123+
// Clarify the assembly source
124+
await Expect.That(Types.InNamespace("MyApp.Domain").InAssemblyContaining<MyApp.Startup>())
125+
.AreSealed();
126+
```
127+
104128
## Navigating to members
105129

106130
From a collection of assemblies or types you can navigate to the members they contain, and from members
@@ -401,11 +425,11 @@ Layering and architecture rules over the types a type references **in its signat
401425

402426
```csharp
403427
// Presentation must not reference the data layer
404-
await Expect.That(In.Namespace("MyApp.Presentation"))
428+
await Expect.That(Types.InNamespace("MyApp.Presentation"))
405429
.DoNotDependOn("MyApp.Data");
406430

407431
// The API layer may only reference the application and domain layers
408-
await Expect.That(In.Namespace("MyApp.Api"))
432+
await Expect.That(Types.InNamespace("MyApp.Api"))
409433
.DependOnlyOn("MyApp.Application", "MyApp.Domain");
410434

411435
// Filter for the types that depend on a namespace
@@ -434,7 +458,7 @@ extends the built-in set). Types you write in authored signatures always do coun
434458
> as `new Infra.Foo()`, static calls and local variables are **not** detected. Function-pointer signatures
435459
> (`delegate*<…>`) are not decomposed either; the types inside them are invisible to dependency assertions.
436460
> Nested types are separate types with their own dependency surface: asserting on `typeof(Outer)` does not
437-
> include what `Outer.Inner` references. The collection-based assertions (e.g. over `In.Namespace(…)`)
461+
> include what `Outer.Inner` references. The collection-based assertions (e.g. over `Types.InNamespace(…)`)
438462
> enumerate nested types as their own items and therefore cover them. For IL/body-level accuracy, plug in
439463
> your own resolver via `Customize.aweXpect.Reflection().DependencyResolver()` (see
440464
> [Configuration](#dependency-resolver)).
@@ -445,7 +469,7 @@ can be targeted or allowed with an empty string (`""`). Each result is chainable
445469

446470
```csharp
447471
// Widen the set with .OrOn(…)
448-
await Expect.That(In.Namespace("MyApp.Api"))
472+
await Expect.That(Types.InNamespace("MyApp.Api"))
449473
.DependOnlyOn("MyApp.Application").OrOn("MyApp.Domain");
450474

451475
// Opt out of sub-namespace matching for the whole expression
@@ -457,7 +481,7 @@ For `DependsOnlyOn` a type's own namespace is always allowed, and by default so
457481
type's own sub-namespaces:
458482

459483
```csharp
460-
await Expect.That(In.Namespace("MyApp.Domain"))
484+
await Expect.That(Types.InNamespace("MyApp.Domain"))
461485
.DependOnlyOn("MyApp.Domain").ExcludingSubNamespaces().ExcludingOwnSubNamespaces();
462486
```
463487

@@ -490,7 +514,7 @@ The "slices should be free of cycles" architecture rule: assert that the namespa
490514

491515
```csharp
492516
// No dependency cycles among the namespaces under MyApp
493-
await Expect.That(In.Namespace("MyApp"))
517+
await Expect.That(Types.InNamespace("MyApp"))
494518
.HaveNoDependencyCycles();
495519
```
496520

@@ -512,7 +536,7 @@ parent/child reference becomes an edge (and can form a cycle):
512536

513537
```csharp
514538
// Treat every namespace as its own node (MyApp.Orders ↔ MyApp.Orders.Domain can now form a cycle)
515-
await Expect.That(In.Namespace("MyApp"))
539+
await Expect.That(Types.InNamespace("MyApp"))
516540
.HaveNoDependencyCycles().ExcludingSubNamespaces();
517541
```
518542

@@ -522,7 +546,7 @@ into the single slice `MyApp.Orders`:
522546

523547
```csharp
524548
// Group MyApp.Orders.* / MyApp.Billing.* / … into one slice each before looking for cycles
525-
await Expect.That(In.Namespace("MyApp"))
549+
await Expect.That(Types.InNamespace("MyApp"))
526550
.HaveNoDependencyCycles("MyApp");
527551
```
528552

Source/aweXpect.Reflection/Collections/Filtered.Types.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,5 +483,65 @@ public NamespaceDependencyOnlyOnFilterResult ExcludingOwnSubNamespaces()
483483
return new NamespaceDependencyOnlyOnFilterResult(refined, _build);
484484
}
485485
}
486+
487+
/// <summary>
488+
/// A filtered collection of <see cref="System.Type" /> within a namespace, that also allows clarifying
489+
/// the assembly source once (it defaults to all loaded assemblies).
490+
/// </summary>
491+
/// <remarks>
492+
/// The clarification methods return a plain <see cref="Types" /> collection rooted at the given source,
493+
/// so the source can only be specified once and must be specified before applying further filters.
494+
/// </remarks>
495+
public sealed class InNamespaceResult : Types
496+
{
497+
private readonly string _namespace;
498+
499+
internal InNamespaceResult(string @namespace)
500+
: base(In.AllLoadedAssemblies().Types().WithinNamespace(@namespace))
501+
{
502+
_namespace = @namespace;
503+
}
504+
505+
/// <summary>
506+
/// Clarifies that the types within the namespace are searched in all loaded assemblies from the
507+
/// current <see cref="System.AppDomain.CurrentDomain" /> (the default).
508+
/// </summary>
509+
public Types InAllLoadedAssemblies()
510+
=> In.AllLoadedAssemblies().Types().WithinNamespace(_namespace);
511+
512+
/// <summary>
513+
/// Clarifies that the types within the namespace are searched in the given <paramref name="assemblies" />.
514+
/// </summary>
515+
public Types InAssemblies(params IEnumerable<Assembly?> assemblies)
516+
=> new Assemblies(assemblies, $"in the assemblies {Formatter.Format(assemblies)}").Types()
517+
.WithinNamespace(_namespace);
518+
519+
/// <summary>
520+
/// Clarifies that the types within the namespace are searched in the assembly that contains
521+
/// the <typeparamref name="TType" />.
522+
/// </summary>
523+
public Types InAssemblyContaining<TType>()
524+
=> In.AssemblyContaining<TType>().Types().WithinNamespace(_namespace);
525+
526+
/// <summary>
527+
/// Clarifies that the types within the namespace are searched in the assembly that contains
528+
/// the <paramref name="type" />.
529+
/// </summary>
530+
public Types InAssemblyContaining(Type type)
531+
=> In.AssemblyContaining(type).Types().WithinNamespace(_namespace);
532+
533+
/// <summary>
534+
/// Clarifies that the types within the namespace are searched in the <see cref="Assembly.GetEntryAssembly()" />.
535+
/// </summary>
536+
public Types InEntryAssembly()
537+
=> In.EntryAssembly().Types().WithinNamespace(_namespace);
538+
539+
/// <summary>
540+
/// Clarifies that the types within the namespace are searched in the
541+
/// <see cref="Assembly.GetExecutingAssembly()" />.
542+
/// </summary>
543+
public Types InExecutingAssembly()
544+
=> In.ExecutingAssembly().Types().WithinNamespace(_namespace);
545+
}
486546
}
487547
}

Source/aweXpect.Reflection/In.cs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,18 +62,6 @@ public static Filtered.Assemblies EntryAssembly()
6262
public static Filtered.Assemblies ExecutingAssembly()
6363
=> new(Assembly.GetExecutingAssembly(), "in executing assembly");
6464

65-
/// <summary>
66-
/// Defines expectations on all types within the <paramref name="namespace" /> (including sub-namespaces)
67-
/// from all loaded assemblies of the current <see cref="System.AppDomain.CurrentDomain" />.
68-
/// </summary>
69-
/// <remarks>
70-
/// This matches the <paramref name="namespace" /> and its sub-namespaces, but not namespaces that merely start
71-
/// with the same text (e.g. <c>Foo.Bar</c> does not include <c>Foo.BarBaz</c>). The comparison is exact and
72-
/// case-sensitive.
73-
/// </remarks>
74-
public static Filtered.Types Namespace(string @namespace)
75-
=> AllLoadedAssemblies().Types().WithinNamespace(@namespace);
76-
7765
/// <summary>
7866
/// Defines expectations on the type <typeparamref name="TType" />.
7967
/// </summary>

Source/aweXpect.Reflection/ThatTypes.HaveNoDependencyCycles.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private sealed class HaveNoDependencyCyclesConstraint(
108108

109109
public ConstraintResult IsMetBy(IEnumerable<Type?> actual)
110110
{
111-
// Materialize once: the source (e.g. In.Namespace(...)) may be lazy and re-scan assemblies on every
111+
// Materialize once: the source (e.g. Types.InNamespace(...)) may be lazy and re-scan assemblies on every
112112
// enumeration, while both the cycle detection and the later result formatting read Actual.
113113
List<Type?> materialized = [.. actual];
114114
Actual = materialized;
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Reflection;
4+
using aweXpect.Reflection.Collections;
5+
6+
namespace aweXpect.Reflection;
7+
8+
/// <summary>
9+
/// Static entry point for selecting types by criteria.
10+
/// </summary>
11+
public static class Types
12+
{
13+
/// <summary>
14+
/// Defines expectations on all types in all loaded assemblies from the current
15+
/// <see cref="System.AppDomain.CurrentDomain" />.
16+
/// </summary>
17+
/// <remarks>
18+
/// Assemblies whose name matches one of the <c>ExcludedAssemblyPrefixes</c> at a name-segment boundary
19+
/// are skipped — the same matching the dependency assertions use, so one customization value has one
20+
/// meaning everywhere.
21+
/// </remarks>
22+
public static Filtered.Types InAllLoadedAssemblies()
23+
=> In.AllLoadedAssemblies().Types();
24+
25+
/// <summary>
26+
/// Defines expectations on all types in the given <paramref name="assemblies" />.
27+
/// </summary>
28+
public static Filtered.Types InAssemblies(params IEnumerable<Assembly?> assemblies)
29+
=> In.Assemblies(assemblies).Types();
30+
31+
/// <summary>
32+
/// Defines expectations on all types in the assembly that contains the <typeparamref name="TType" />.
33+
/// </summary>
34+
public static Filtered.Types InAssemblyContaining<TType>()
35+
=> In.AssemblyContaining<TType>().Types();
36+
37+
/// <summary>
38+
/// Defines expectations on all types in the assembly that contains the <paramref name="type" />.
39+
/// </summary>
40+
public static Filtered.Types InAssemblyContaining(Type type)
41+
=> In.AssemblyContaining(type).Types();
42+
43+
/// <summary>
44+
/// Defines expectations on all types in the <see cref="Assembly.GetEntryAssembly()" />.
45+
/// </summary>
46+
public static Filtered.Types InEntryAssembly()
47+
=> In.EntryAssembly().Types();
48+
49+
/// <summary>
50+
/// Defines expectations on all types in the <see cref="Assembly.GetExecutingAssembly()" />.
51+
/// </summary>
52+
public static Filtered.Types InExecutingAssembly()
53+
=> In.ExecutingAssembly().Types();
54+
55+
/// <summary>
56+
/// Defines expectations on all types within the <paramref name="namespace" /> (including sub-namespaces).
57+
/// </summary>
58+
/// <remarks>
59+
/// By default, the types from all loaded assemblies of the current
60+
/// <see cref="System.AppDomain.CurrentDomain" /> are considered; use one of the <c>In*</c> methods on the
61+
/// returned collection to clarify a different assembly source.
62+
/// <para />
63+
/// This matches the <paramref name="namespace" /> and its sub-namespaces, but not namespaces that merely start
64+
/// with the same text (e.g. <c>Foo.Bar</c> does not include <c>Foo.BarBaz</c>). The comparison is exact and
65+
/// case-sensitive.
66+
/// </remarks>
67+
public static Filtered.Types.InNamespaceResult InNamespace(string @namespace)
68+
=> new(@namespace);
69+
}

Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net10.0.txt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ namespace aweXpect.Reflection
329329
public static aweXpect.Reflection.Collections.Filtered.Assemblies ExecutingAssembly() { }
330330
public static aweXpect.Reflection.Collections.Filtered.Fields Fields(System.Collections.Generic.IEnumerable<System.Reflection.FieldInfo> fields) { }
331331
public static aweXpect.Reflection.Collections.Filtered.Methods Methods(System.Collections.Generic.IEnumerable<System.Reflection.MethodInfo> methods) { }
332-
public static aweXpect.Reflection.Collections.Filtered.Types Namespace(string @namespace) { }
333332
public static aweXpect.Reflection.Collections.Filtered.Properties Properties(System.Collections.Generic.IEnumerable<System.Reflection.PropertyInfo> properties) { }
334333
public static aweXpect.Reflection.Collections.Filtered.Types Type(System.Type type) { }
335334
public static aweXpect.Reflection.Collections.Filtered.Types Type<TType>() { }
@@ -2019,6 +2018,16 @@ namespace aweXpect.Reflection
20192018
where TAttribute : System.Attribute { }
20202019
}
20212020
}
2021+
public static class Types
2022+
{
2023+
public static aweXpect.Reflection.Collections.Filtered.Types InAllLoadedAssemblies() { }
2024+
public static aweXpect.Reflection.Collections.Filtered.Types InAssemblies(System.Collections.Generic.IEnumerable<System.Reflection.Assembly?> assemblies) { }
2025+
public static aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining(System.Type type) { }
2026+
public static aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining<TType>() { }
2027+
public static aweXpect.Reflection.Collections.Filtered.Types InEntryAssembly() { }
2028+
public static aweXpect.Reflection.Collections.Filtered.Types InExecutingAssembly() { }
2029+
public static aweXpect.Reflection.Collections.Filtered.Types.InNamespaceResult InNamespace(string @namespace) { }
2030+
}
20222031
}
20232032
namespace aweXpect.Reflection.Collections
20242033
{
@@ -2186,6 +2195,15 @@ namespace aweXpect.Reflection.Collections
21862195
public string GetDescription() { }
21872196
public aweXpect.Reflection.Collections.Filtered.Methods Methods(aweXpect.Reflection.Collections.MemberScope memberScope = 0) { }
21882197
public aweXpect.Reflection.Collections.Filtered.Properties Properties(aweXpect.Reflection.Collections.MemberScope memberScope = 0) { }
2198+
public sealed class InNamespaceResult : aweXpect.Reflection.Collections.Filtered.Types
2199+
{
2200+
public aweXpect.Reflection.Collections.Filtered.Types InAllLoadedAssemblies() { }
2201+
public aweXpect.Reflection.Collections.Filtered.Types InAssemblies(System.Collections.Generic.IEnumerable<System.Reflection.Assembly?> assemblies) { }
2202+
public aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining(System.Type type) { }
2203+
public aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining<TType>() { }
2204+
public aweXpect.Reflection.Collections.Filtered.Types InEntryAssembly() { }
2205+
public aweXpect.Reflection.Collections.Filtered.Types InExecutingAssembly() { }
2206+
}
21892207
public sealed class NamespaceDependencyFilterResult : aweXpect.Reflection.Collections.Filtered.Types
21902208
{
21912209
public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult ExcludingSubNamespaces() { }

Tests/aweXpect.Reflection.Api.Tests/Expected/aweXpect.Reflection_net8.0.txt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,6 @@ namespace aweXpect.Reflection
329329
public static aweXpect.Reflection.Collections.Filtered.Assemblies ExecutingAssembly() { }
330330
public static aweXpect.Reflection.Collections.Filtered.Fields Fields(System.Collections.Generic.IEnumerable<System.Reflection.FieldInfo> fields) { }
331331
public static aweXpect.Reflection.Collections.Filtered.Methods Methods(System.Collections.Generic.IEnumerable<System.Reflection.MethodInfo> methods) { }
332-
public static aweXpect.Reflection.Collections.Filtered.Types Namespace(string @namespace) { }
333332
public static aweXpect.Reflection.Collections.Filtered.Properties Properties(System.Collections.Generic.IEnumerable<System.Reflection.PropertyInfo> properties) { }
334333
public static aweXpect.Reflection.Collections.Filtered.Types Type(System.Type type) { }
335334
public static aweXpect.Reflection.Collections.Filtered.Types Type<TType>() { }
@@ -2019,6 +2018,16 @@ namespace aweXpect.Reflection
20192018
where TAttribute : System.Attribute { }
20202019
}
20212020
}
2021+
public static class Types
2022+
{
2023+
public static aweXpect.Reflection.Collections.Filtered.Types InAllLoadedAssemblies() { }
2024+
public static aweXpect.Reflection.Collections.Filtered.Types InAssemblies(System.Collections.Generic.IEnumerable<System.Reflection.Assembly?> assemblies) { }
2025+
public static aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining(System.Type type) { }
2026+
public static aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining<TType>() { }
2027+
public static aweXpect.Reflection.Collections.Filtered.Types InEntryAssembly() { }
2028+
public static aweXpect.Reflection.Collections.Filtered.Types InExecutingAssembly() { }
2029+
public static aweXpect.Reflection.Collections.Filtered.Types.InNamespaceResult InNamespace(string @namespace) { }
2030+
}
20222031
}
20232032
namespace aweXpect.Reflection.Collections
20242033
{
@@ -2186,6 +2195,15 @@ namespace aweXpect.Reflection.Collections
21862195
public string GetDescription() { }
21872196
public aweXpect.Reflection.Collections.Filtered.Methods Methods(aweXpect.Reflection.Collections.MemberScope memberScope = 0) { }
21882197
public aweXpect.Reflection.Collections.Filtered.Properties Properties(aweXpect.Reflection.Collections.MemberScope memberScope = 0) { }
2198+
public sealed class InNamespaceResult : aweXpect.Reflection.Collections.Filtered.Types
2199+
{
2200+
public aweXpect.Reflection.Collections.Filtered.Types InAllLoadedAssemblies() { }
2201+
public aweXpect.Reflection.Collections.Filtered.Types InAssemblies(System.Collections.Generic.IEnumerable<System.Reflection.Assembly?> assemblies) { }
2202+
public aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining(System.Type type) { }
2203+
public aweXpect.Reflection.Collections.Filtered.Types InAssemblyContaining<TType>() { }
2204+
public aweXpect.Reflection.Collections.Filtered.Types InEntryAssembly() { }
2205+
public aweXpect.Reflection.Collections.Filtered.Types InExecutingAssembly() { }
2206+
}
21892207
public sealed class NamespaceDependencyFilterResult : aweXpect.Reflection.Collections.Filtered.Types
21902208
{
21912209
public aweXpect.Reflection.Collections.Filtered.Types.NamespaceDependencyFilterResult ExcludingSubNamespaces() { }

0 commit comments

Comments
 (0)