Configuration
All customizations live under Customize.aweXpect.Reflection(). Every Set(…) returns a scope that
restores the previous value when disposed, so a customization can be applied globally or per test.
Assembly exclusions
By default, assemblies whose name matches one of the following prefixes are excluded from
In.AllLoadedAssemblies():
mscorlib, System, Microsoft, netstandard, WindowsBase, JetBrains, xunit, Castle,
DynamicProxyGenAssembly2.
Both the assembly scanning and the dependency assertions (DependsOnlyOn / DependOnlyOn /
WhichDependOnlyOn, on both assemblies and types) use the same prefixes with the same matching: a prefix
matches at a name-segment boundary, so System covers System and System.Text.Json, but not an assembly
named SystemsBiology. A prefix written with a trailing dot (e.g. MyCompany.) is boundary-safe by
construction and covers everything starting with it. Empty prefixes are ignored.
Customize this via Customize.aweXpect.Reflection().ExcludedAssemblyPrefixes. Set(…) replaces the
list and returns a scope that restores the previous value when disposed:
using (Customize.aweXpect.Reflection().ExcludedAssemblyPrefixes
.Set(new[] { "mscorlib", "System", "Microsoft", "MyCompany.Generated" }))
{
// In.AllLoadedAssemblies() applies the custom prefixes within this scope
}
Compiler-generated members
By default, compiler-generated types and members are excluded from every navigation
(.Types(), .Methods(), .Fields(), …). This hides closures, async/iterator state machines,
anonymous types, local functions, auto-property backing fields and the generated members of records
(ToString, Equals, <Clone>$, the copy-constructor, …), so convention tests see only the members
you actually wrote.
Opt specific kinds back in with the [Flags] enum CompilerGeneratedMembers
(None, Types, Constructors, Methods, Properties, Fields, Events, All):
using (Customize.aweXpect.Reflection().IncludedCompilerGeneratedMembers()
.Set(CompilerGeneratedMembers.Types | CompilerGeneratedMembers.Methods))
{
// closures, state machines and compiler-generated methods are now visible
}
Operators (op_*) and property/event accessors (get_, set_, add_, remove_) are user-written
but likewise excluded by default. Include them via the separate SpecialNameMembers enum
(None, Operators, Accessors, All), which only affects .Methods():
using (Customize.aweXpect.Reflection().IncludedSpecialNameMembers()
.Set(SpecialNameMembers.Operators))
{
// operator methods are now visible in .Methods()
}
Dependency resolver
The type-level dependency assertions compute a type's dependencies with a built-in signature-level resolver (base type, interfaces, field/property/event types, method/constructor signatures, generic arguments and applied attributes). Method-body references are not detected by the default; supply a custom resolver, e.g. backed by Mono.Cecil (this library takes no dependency on it; reference the package yourself), for IL/body-level accuracy:
// Replace the resolver within a scope
using (Customize.aweXpect.Reflection().DependencyResolver()
.Set(type => MyCecilResolver.GetUsedTypes(type)))
{
// body-level references now count as dependencies
}
// Or augment instead of replace: compose on the current default
var resolver = Customize.aweXpect.Reflection().DependencyResolver();
var builtin = resolver.Get()!;
using (resolver.Set(type => builtin(type).Concat(MyCecilResolver.GetBodyTypes(type))))
{
// built-in signature dependencies plus the body-level extras
}
// Setting null reverts to the built-in default, e.g. to opt a single test out
// of a globally configured resolver
using (Customize.aweXpect.Reflection().DependencyResolver().Set(null))
{
// the signature-level default applies within this scope
}
Get() always returns the resolver currently in effect (the built-in default when none is configured),
so composing on it works regardless of what an outer scope has set up.
A Mono.Cecil-backed resolver boils down to reading the type's assembly and mapping the IL references back to runtime types:
public static IEnumerable<Type> GetUsedTypes(Type type)
{
using var assembly = Mono.Cecil.AssemblyDefinition.ReadAssembly(type.Assembly.Location);
var definition = assembly.MainModule.GetType(type.FullName!.Replace('+', '/'));
foreach (var instruction in definition.Methods
.Where(method => method.HasBody)
.SelectMany(method => method.Body.Instructions))
{
// map the method/field/type references in instruction.Operand back to System.Type
// (and also walk the signature surface: base type, interfaces, fields, …)
}
}
Every resolver's output is normalized like the built-in's (array/by-ref/pointer element types and
generic arguments are unwrapped, the result is de-duplicated) and cached per type for the lifetime of
the resolver delegate, so a custom resolver needs no caching of its own. It must, however, be pure:
deterministic for a given Type within its scope; that is what makes the caching safe.